Compare commits

...

27 Commits

Author SHA1 Message Date
Manuel Vögele 4b68b82590 Release v1.5.1 2021-04-15 17:19:32 +02:00
Manuel Vögele 447995977b Don't show the difficult terrain measurement hint when no terrain module is enabled 2021-04-15 17:17:38 +02:00
Manuel Vögele 9e5b344b0d Release v1.5.0 2021-04-14 22:51:05 +02:00
Manuel Vögele 43c5df42d4 If a terrain module is installed, suggest installing Terrain Ruler to the user 2021-04-14 22:49:18 +02:00
Manuel Vögele d0ee47d551 Fix typo that made regular rulers not work 2021-04-14 19:45:17 +02:00
Manuel Vögele 27ffb2d8cb Don't return any value in _socketUpdateCombatantDragRulerFlags to reduce the amount of network traffic 2021-04-14 17:32:40 +02:00
Manuel Vögele b734257515 Check if all required dependencies are present and show an error if this is not the case 2021-04-14 16:37:46 +02:00
Manuel Vögele ff5d46ebbe Add a setting to disable the movement history 2021-04-14 16:10:11 +02:00
Manuel Vögele 8f896d9b82 Fix a bug that caused Drag Ruler to measure euclidic distance when Terrain Ruler isn't active 2021-04-14 15:54:59 +02:00
Manuel Vögele f7683e8945 Update the movement tracking flags in the GM client, becauase the user isn't allowed to modify them 2021-04-14 15:54:59 +02:00
Manuel Vögele 9787a41fd1 If Terran Ruler is enabled, store the length of the traveled path so changes to difficult terrain aren't reflected in the tokens movement history 2021-04-12 18:45:57 +02:00
Manuel Vögele 5d93c61f0c Merge branch 'master' into develop 2021-04-11 14:31:44 +02:00
Manuel Vögele 00e8a9781e Release v1.4.6 2021-04-11 14:27:09 +02:00
Manuel Vögele d2f2307e4d Update the ruler before dropping the token to get a more precise target location if the token has been moved very quickly (fixes #51) 2021-04-11 14:24:23 +02:00
Manuel Vögele 65912df989 Add support for the Enhanced Terrain Layer module (resolves #50) 2021-04-10 15:02:35 +02:00
Manuel Vögele 9ae9dae427 Add c66ec46 to changelog 2021-04-10 15:00:48 +02:00
Manuel Vögele 8cd5d7f276 Migrate Drag Ruler to Terrain Ruler's new API 2021-04-10 14:43:45 +02:00
Manuel Vögele 35624a37aa Add api endpoint to determine the distance that a token has moved already 2021-04-10 14:25:01 +02:00
Manuel Vögele c66ec46aed Track the path taken by tokens in combat and display it when reactivating DragRuler in the same turn 2021-04-08 09:35:04 +02:00
Manuel Vögele dec422fa3a Fix typo in changelog 2021-04-01 22:57:13 +02:00
Manuel Vögele 6ad1f4c522 Merge branch 'master' into develop 2021-04-01 22:36:06 +02:00
Manuel Vögele fb9a2ee60c Release v1.4.5 2021-04-01 22:35:23 +02:00
Manuel Vögele 57ed545e30 Snap tiny tokens (0.5x0.5 or smaller) to the corners of the grid squares like it is done in vanilla foundry (fixes #49) 2021-04-01 22:25:21 +02:00
Manuel Vögele 94fe223529 Fix measurement errors when disabling token snapping while the Terrain Ruler module isn't enabled 2021-04-01 00:07:26 +02:00
Manuel Vögele 305a6116bc Refactor: Override ruler class instead of hooking each function individually 2021-03-31 17:19:12 +02:00
Manuel Vögele 942bfa04e8 Release v1.4.4 2021-03-25 01:57:41 +01:00
Manuel Vögele b4ac65e478 Fix snapping for tokens that are smaller than 1x1 (fixes #48) 2021-03-25 01:55:48 +01:00
14 changed files with 454 additions and 129 deletions
+30
View File
@@ -1,3 +1,33 @@
## 1.5.1
### Bugfixes
- The hint that tells users how to enable difficult terrain measurement in Drag Ruler is no longer shown if no terrain layer module is installed.
## 1.5.0
### New features
- In combat Drag Ruler will now remember the path that was taken by a token during the turn. Picking the token up during the same turn will continue the previous measurement, taking steps that are already taken into account.
### Module compatibility
- Drag Ruler is now fully compatible with the `Enhanced Terrain Layer` module. ([#50](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/50))
## 1.4.6
### Bugfixes
- Fixed a bug where a token would move to the incorrect location if it is was dragged and released very quickly ([#51](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/51)) - thanks to Silver Raven for helping me track down this bug!
## 1.4.5
### Bugfixes
- Tiny tokens (0.5x0.5 or smaller) now snap to the coners of a square like they do in vanilla foundry ([#49](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/49))
- Fixed a bug that could cause a meausred distance to be wrong when disabling token snapping using the shift key
- Fixed a bug where the highlighted path could have gaps when disabling token snapping using the shift key
## 1.4.4
### Bugfixes
- Fix snapping for tokens that are smaller than 1x1
## 1.4.3 ## 1.4.3
### System compatibility ### System compatibility
- Drag Ruler's Generic SpeedProvider is now aware of good default values for the Savage Worlds Adventure Edition game system - Drag Ruler's Generic SpeedProvider is now aware of good default values for the Savage Worlds Adventure Edition game system
+16
View File
@@ -1,5 +1,17 @@
{ {
"drag-ruler": { "drag-ruler": {
"dependencies": {
"ok": "OK",
"terrain-ruler": {
"title": "Wie man schwieriges Gelände mit Drag Ruler misst",
"text": "Du hast das {moduleName} Modul installiert. Drag Ruler kann schwieriges Gelände, das mit diesem Modul platziert wurde in seinen Messungen berücksichtigen. Hierzu greift Drag Ruler auf die Funktionalität von Terrain Ruler zurück. Wenn Terrain Ruler installiert und aktiviert ist wird Drag Ruler automatisch anfangen schwiriges Gelände in den Messungen zu berücksichtigen.",
"neverShowAgain": "Zeige dies nie wieder an"
},
"socketlib": {
"title": "Socketlib Modul fehlt",
"text": "Drag Ruler benötigt das socketlib-Modul, um korrekt zu funktionieren. Bitte aktiviere das socketlib-Modul in dieser Welt."
}
},
"genericSpeedProvider": { "genericSpeedProvider": {
"settings": { "settings": {
"dashMultiplier": { "dashMultiplier": {
@@ -21,6 +33,10 @@
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen", "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." "hint": "Wenn diese Einstellung aktiviert ist wird die Färbung der hervorgehobenen Felder bei Spielercharakteren allen Spielern angezeigt, selbst wenn diese keinen Zugriff auf den Charakterbogen haben."
}, },
"enableMovementHistory": {
"name": "Bewegungsverlauf während des Kampfes aktivieren",
"hint": "Wenn diesen Option aktiviert ist merkt sich Drag Ruler die Bewegungen, die während einer Runde eines Kampfes gemacht wurden und zeigt diese an, wenn das Token weiterbewegt wird."
},
"showGMRulerToPlayers": { "showGMRulerToPlayers": {
"name": "Lineal des Sipelleiters bei Spielern anzeigen", "name": "Lineal des Sipelleiters bei Spielern anzeigen",
"hint": "Wenn diese Option deaktiviert ist wird das Lineal des Spielleiters bei den anderen Spielern nicht angezeigt." "hint": "Wenn diese Option deaktiviert ist wird das Lineal des Spielleiters bei den anderen Spielern nicht angezeigt."
+16
View File
@@ -1,5 +1,17 @@
{ {
"drag-ruler": { "drag-ruler": {
"dependencies": {
"ok": "OK",
"terrain-ruler": {
"title": "How to measure difficult terrain with Drag Ruler",
"text": "You have the {moduleName} module enabled. Drag Ruler is able to measure difficult terrain that was placed down using that module. To make this possible Drag Ruler utilizes the Terrain Ruler module. If Terrain Ruler is installed and activated Drag Ruler will automatically start to respect difficult terrain in it's measurements.",
"neverShowAgain": "Never show this again"
},
"socketlib": {
"title": "Socketlib Module Missing",
"text": "Drag Ruler requires the socketlib module to work properly. Please activate the socketlib module in this world."
}
},
"genericSpeedProvider": { "genericSpeedProvider": {
"settings": { "settings": {
"dashMultiplier": { "dashMultiplier": {
@@ -21,6 +33,10 @@
"name": "Show PC speed to everyone", "name": "Show PC speed to everyone",
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet." "hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
}, },
"enableMovementHistory": {
"name": "Enable movement history during combat",
"hint": "If enabled, Drag Ruler will remember the path a token took during it's turn in combat and will display it when you pick the token back up."
},
"showGMRulerToPlayers": { "showGMRulerToPlayers": {
"name": "Show GM ruler to players", "name": "Show GM ruler to players",
"hint": "If disabled the ruler of GMs will not be shown for non-GM players" "hint": "If disabled the ruler of GMs will not be shown for non-GM players"
+11 -3
View File
@@ -2,7 +2,7 @@
"name": "drag-ruler", "name": "drag-ruler",
"title": "Drag Ruler", "title": "Drag Ruler",
"description": "When dragging a token displays a ruler showing how far you've moved that token.", "description": "When dragging a token displays a ruler showing how far you've moved that token.",
"version": "1.4.3", "version": "1.5.1",
"minimumCoreVersion" : "0.7.9", "minimumCoreVersion" : "0.7.9",
"compatibleCoreVersion" : "0.7.9", "compatibleCoreVersion" : "0.7.9",
"authors": [ "authors": [
@@ -13,7 +13,8 @@
} }
], ],
"esmodules": [ "esmodules": [
"src/main.js" "src/main.js",
"src/socket.js"
], ],
"templates": [ "templates": [
"speed_provider_settings.html" "speed_provider_settings.html"
@@ -40,8 +41,15 @@
"path": "lang/zh-tw.json" "path": "lang/zh-tw.json"
} }
], ],
"dependencies": [
{
"name": "socketlib",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
}
],
"socket": true,
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler", "url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.4.3.zip", "download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.5.1.zip",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json", "manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md", "readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md", "changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
+12
View File
@@ -1,5 +1,8 @@
import {measureDistances} from "./compatibility.js";
import {getMovementHistory} from "./movement_tracking.js";
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js" import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.js" import {settingsKey} from "./settings.js"
import {getTokenShape} from "./util.js";
export const availableSpeedProviders = {} export const availableSpeedProviders = {}
export let currentSpeedProvider = undefined export let currentSpeedProvider = undefined
@@ -104,6 +107,15 @@ export function getCostFromSpeedProvider(token, area) {
return currentSpeedProvider.getCostForStep(token, area); return currentSpeedProvider.getCostForStep(token, area);
} }
export function getMovedDistanceFromToken(token) {
const history = getMovementHistory(token);
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
const shape = getTokenShape(token);
const distances = measureDistances(segments, token, shape);
// Sum up the distances
return distances.reduce((acc, val) => acc + val, 0);
}
export function registerModule(moduleId, speedProvider) { export function registerModule(moduleId, speedProvider) {
// Check if a module with the given id exists and is currently enabled // 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)
+75 -3
View File
@@ -1,14 +1,86 @@
import {getCostFromSpeedProvider} from "./api.js";
import {getColorForDistance} from "./main.js" import {getColorForDistance} from "./main.js"
import {highlightTokenShape} from "./util.js" import {settingsKey} from "./settings.js";
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
export function getHexSizeSupportTokenGridCenter(token) { export function getHexSizeSupportTokenGridCenter(token) {
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token) const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y} return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
} }
export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}]) { export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
for (const space of ray.terrainRulerVisitedSpaces.reverse()) { for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
const color = getColorForDistance.call(this, startDistance, space.distance) const color = getColorForDistance.call(this, startDistance, space.distance)
highlightTokenShape.call(this, space, tokenShape, color) highlightTokenShape.call(this, space, tokenShape, color, alpha)
} }
} }
export function measureDistances(segments, token, shape, gridSpaces=true, options={}) {
const opts = duplicate(options)
opts.gridSpaces = gridSpaces;
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
if (terrainRulerAvailable) {
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
const previousSegments = segments.slice(0, firstNewSegmentIndex);
const newSegments = segments.slice(firstNewSegmentIndex);
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
opts.costFunction = (x, y) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), {x, y});
if (previousSegments.length > 0)
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1];
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
}
else {
return canvas.grid.measureDistances(segments, opts);
}
}
export function checkDependencies() {
if (!game.modules.get("socketlib")?.active) {
console.error("Drag Ruler | The `socketlib` module isn't enabled, but it's required for Drag Ruler to operate properly.");
if (game.user.isGM) {
new Dialog({
title: game.i18n.localize("drag-ruler.dependencies.socketlib.title"),
content: `<h2>${game.i18n.localize("drag-ruler.dependencies.socketlib.title")}</h2><p>${game.i18n.localize("drag-ruler.dependencies.socketlib.text")}</p>`,
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("drag-ruler.dependencies.ok")
}
},
}).render(true);
}
}
else if (!game.modules.get("terrain-ruler")?.active && game.user.isGM && !game.settings.get(settingsKey, "neverShowTerrainRulerHint")) {
const lastHint = game.settings.get(settingsKey, "lastTerrainRulerHintTime");
if (Date.now() - lastHint > 604800000) { // One week
let enabledTerrainModule;
if (game.modules.get("enhanced-terrain-layer")?.active) {
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
}
else if (game.modules.get("TerrainLayer")?.active) {
enabledTerrainModule = game.modules.get("TerrainLayer").data.title;
}
if (enabledTerrainModule) {
new Dialog({
title: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title"),
content: `<h2>${game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title")}</h2><p>${game.i18n.format("drag-ruler.dependencies.terrain-ruler.text", {moduleName: enabledTerrainModule})}</p>`,
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("drag-ruler.dependencies.ok"),
callback: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
},
neverShowAgain: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.neverShowAgain"),
callback: () => game.settings.set(settingsKey, "neverShowTerrainRulerHint", true),
}
},
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now())
}).render(true);
}
}
}
}
+30 -20
View File
@@ -1,8 +1,8 @@
import { getCostFromSpeedProvider } from "./api.js"; import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
import {highlightMeasurementTerrainRuler} from "./compatibility.js";
import {getGridPositionFromPixels} from "./foundry_fixes.js"; import {getGridPositionFromPixels} from "./foundry_fixes.js";
import {getColorForDistance} from "./main.js" import {getColorForDistance} from "./main.js"
import {applyTokenSizeOffset, getAreaFromPositionAndShape, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js"; import {trackRays} from "./movement_tracking.js"
import {applyTokenSizeOffset, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
// This is a modified version of Ruler.moveToken from foundry 0.7.9 // This is a modified version of Ruler.moveToken from foundry 0.7.9
export async function moveTokens(draggedToken, selectedTokens) { export async function moveTokens(draggedToken, selectedTokens) {
@@ -16,7 +16,7 @@ export async function moveTokens(draggedToken, selectedTokens) {
// Get the movement rays and check collision along each Ray // Get the movement rays and check collision along each Ray
// These rays are center-to-center for the purposes of collision checking // These rays are center-to-center for the purposes of collision checking
const rays = this._getRaysFromWaypoints(this.waypoints, this.destination); const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
if (!game.user.isGM) { if (!game.user.isGM) {
const hasCollision = selectedTokens.some(token => { const hasCollision = selectedTokens.some(token => {
const offset = calculateTokenOffset(token, draggedToken) const offset = calculateTokenOffset(token, draggedToken)
@@ -45,11 +45,13 @@ export async function moveTokens(draggedToken, selectedTokens) {
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9 // This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
async function animateToken(token, rays, tokenOffset, wasPaused) { async function animateToken(token, rays, tokenOffset, wasPaused) {
const offsetRays = rays.map(ray => applyOffsetToRay(ray, tokenOffset)) const offsetRays = rays.filter(r => !r.isPrevious).map(ray => applyOffsetToRay(ray, tokenOffset));
trackRays(token, offsetRays);
// Determine offset relative to the Token top-left. // Determine offset relative to the Token top-left.
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens. // This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
const origin = [this.waypoints[0].x + tokenOffset.x, this.waypoints[0].y + tokenOffset.y] const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
const origin = [firstWaypoint.x + tokenOffset.x, firstWaypoint.y + tokenOffset.y];
let dx, dy let dx, dy
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
dx = token.data.x - origin[0] dx = token.data.x - origin[0]
@@ -76,7 +78,9 @@ function calculateTokenOffset(tokenA, tokenB) {
} }
function applyOffsetToRay(ray, offset) { function applyOffsetToRay(ray, offset) {
return new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y}) const newRay = new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y});
newRay.isPrevious = ray.isPrevious;
return newRay;
} }
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9 // This is a modified version of Ruler._onMouseMove from foundry 0.7.9
@@ -108,9 +112,14 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
if (snap) if (snap)
destination = getSnapPointForToken(destination.x, destination.y, this.draggedToken) destination = getSnapPointForToken(destination.x, destination.y, this.draggedToken)
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
const waypoints = this.waypoints.concat([destination]); const waypoints = this.waypoints.concat([destination]);
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge // Move the waypoints to the center of the grid if a size is used that measures from edge to edge
const centeredWaypoints = applyTokenSizeOffset(waypoints, this.draggedToken) const centeredWaypoints = applyTokenSizeOffset(waypoints, this.draggedToken)
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
if (!terrainRulerAvailable)
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
const r = this.ruler; const r = this.ruler;
this.destination = destination; this.destination = destination;
@@ -125,6 +134,10 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
const label = this.labels.children[i]; const label = this.labels.children[i];
const ray = new Ray(origin, dest); const ray = new Ray(origin, dest);
const centeredRay = new Ray(centeredOrigin, centeredDest) const centeredRay = new Ray(centeredOrigin, centeredDest)
ray.isPrevious = Boolean(origin.isPrevious);
centeredRay.isPrevious = ray.isPrevious;
ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces;
centeredRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
if (ray.distance < 10) { if (ray.distance < 10) {
if (label) label.visible = false; if (label) label.visible = false;
continue; continue;
@@ -133,15 +146,11 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
centeredSegments.push({ray: centeredRay, label}) centeredSegments.push({ray: centeredRay, label})
} }
const shape = getTokenShape(this.draggedToken) const shape = getTokenShape(this.draggedToken)
// Compute measured distance // Compute measured distance
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS const distances = measureDistances(centeredSegments, this.draggedToken, shape, gridSpaces);
let distances
if (terrainRulerAvailable)
distances = game.terrainRuler.measureDistances(centeredSegments, {costFunction: (x, y) => getCostFromSpeedProvider(this.draggedToken, getAreaFromPositionAndShape({x, y}, shape), {x, y})});
else
distances = canvas.grid.measureDistances(centeredSegments, { gridSpaces });
let totalDistance = 0; let totalDistance = 0;
for (let [i, d] of distances.entries()) { for (let [i, d] of distances.entries()) {
@@ -168,8 +177,9 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
const { label, text, last } = cs; const { label, text, last } = cs;
// Draw line segment // Draw line segment
r.lineStyle(6, 0x000000, 0.5).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y) const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1;
.lineStyle(4, rulerColor, 0.25).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y); r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y)
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y);
// Draw the distance label just after the endpoint of the segment // Draw the distance label just after the endpoint of the segment
if (label) { if (label) {
@@ -182,9 +192,9 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
// Highlight grid positions // Highlight grid positions
if (terrainRulerAvailable) if (terrainRulerAvailable)
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape) highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
else else
highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape); highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier);
} }
// Draw endpoints // Draw endpoints
@@ -196,7 +206,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
return segments; return segments;
} }
export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}]) { export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1; const 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 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); const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
@@ -232,9 +242,9 @@ export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance) const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken); const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken);
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1); const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color); highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
} }
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color); highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
} }
} }
+18 -99
View File
@@ -1,22 +1,25 @@
"use strict" "use strict"
import {currentSpeedProvider, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js" import {currentSpeedProvider, getMovedDistanceFromToken, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js" import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
import {measure, moveTokens, onMouseMove} from "./foundry_imports.js" import {moveTokens, onMouseMove} from "./foundry_imports.js"
import {performMigrations} from "./migration.js" import {performMigrations} from "./migration.js"
import {DragRulerRuler} from "./ruler.js";
import {getMovementHistory} from "./movement_tracking.js";
import {registerSettings, settingsKey} from "./settings.js" import {registerSettings, settingsKey} from "./settings.js"
import {SpeedProvider} from "./speed_provider.js" import {SpeedProvider} from "./speed_provider.js"
import { getSnapPointForToken } from "./util.js"
Hooks.once("init", () => { Hooks.once("init", () => {
registerSettings() registerSettings()
initApi() initApi()
hookTokenDragHandlers() hookTokenDragHandlers()
hookRulerFunctions()
hookKeyboardManagerFunctions() hookKeyboardManagerFunctions()
Ruler = DragRulerRuler;
window.dragRuler = { window.dragRuler = {
getColorForDistance, getColorForDistance,
getMovedDistanceFromToken,
registerModule, registerModule,
registerSystem, registerSystem,
} }
@@ -24,6 +27,7 @@ Hooks.once("init", () => {
Hooks.once("ready", () => { Hooks.once("ready", () => {
performMigrations() performMigrations()
checkDependencies();
Hooks.callAll("dragRuler.ready", SpeedProvider) Hooks.callAll("dragRuler.ready", SpeedProvider)
}) })
@@ -66,51 +70,6 @@ function hookTokenDragHandlers() {
} }
} }
function hookRulerFunctions() {
const originalMoveTokenHandler = Ruler.prototype.moveToken
Ruler.prototype.moveToken = function (event) {
const eventHandled = onRulerMoveToken.call(this, event)
if (!eventHandled)
return originalMoveTokenHandler.call(this, event)
return true
}
const originalToJSON = Ruler.prototype.toJSON
Ruler.prototype.toJSON = function () {
const json = originalToJSON.call(this)
if (this.draggedToken)
json["draggedToken"] = this.draggedToken.data._id
return json
}
const originalUpdate = Ruler.prototype.update
Ruler.prototype.update = function (data) {
// Don't show a GMs drag ruler to non GM players
if (data.draggedToken && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
return
if (data.draggedToken) {
this.draggedToken = canvas.tokens.get(data.draggedToken)
}
originalUpdate.call(this, data)
}
const originalMeasure = Ruler.prototype.measure
Ruler.prototype.measure = function (destination, options={}) {
if (this.isDragRuler) {
return measure.call(this, destination, options)
}
else {
return originalMeasure.call(this, destination, options)
}
}
const originalEndMeasurement = Ruler.prototype._endMeasurement
Ruler.prototype._endMeasurement = function () {
originalEndMeasurement.call(this)
this.draggedToken = null
}
}
function hookKeyboardManagerFunctions() { function hookKeyboardManagerFunctions() {
const originalHandleKeys = KeyboardManager.prototype._handleKeys const originalHandleKeys = KeyboardManager.prototype._handleKeys
KeyboardManager.prototype._handleKeys = function (event, key, up) { KeyboardManager.prototype._handleKeys = function (event, key, up) {
@@ -132,10 +91,11 @@ function handleKeys(event, key, up) {
function onKeyX(up) { function onKeyX(up) {
if (up) if (up)
return false return false
if (!canvas.controls.ruler.isDragRuler) const ruler = canvas.controls.ruler;
if (!ruler.isDragRuler)
return false return false
deleteWaypoint() ruler.dragRulerDeleteWaypoint();
return true return true
} }
@@ -163,7 +123,9 @@ function onTokenLeftDragStart(event) {
ruler.clear(); ruler.clear();
ruler._state = Ruler.STATES.STARTING; ruler._state = Ruler.STATES.STARTING;
ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y} ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y}
addWaypoint.call(ruler, tokenCenter, false) if (game.settings.get(settingsKey, "enableMovementHistory"))
ruler.dragRulerAddWaypointHistory(getMovementHistory(this));
ruler.dragRulerAddWaypoint(tokenCenter, false);
} }
function onTokenLeftDragMove(event) { function onTokenLeftDragMove(event) {
@@ -176,6 +138,7 @@ function onTokenDragLeftDrop(event) {
const ruler = canvas.controls.ruler const ruler = canvas.controls.ruler
if (!ruler.isDragRuler) if (!ruler.isDragRuler)
return false return false
onMouseMove.call(ruler, event);
const selectedTokens = canvas.tokens.controlled const selectedTokens = canvas.tokens.controlled
ruler._state = Ruler.STATES.MOVING ruler._state = Ruler.STATES.MOVING
moveTokens.call(ruler, ruler.draggedToken, selectedTokens) moveTokens.call(ruler, ruler.draggedToken, selectedTokens)
@@ -189,61 +152,17 @@ function onTokenDragLeftCancel(event) {
return false return false
if (ruler._state === Ruler.STATES.MEASURING) { if (ruler._state === Ruler.STATES.MEASURING) {
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) { if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
if (ruler.waypoints.length > 1) ruler.dragRulerDeleteWaypoint(event);
event.preventDefault()
deleteWaypoint()
} }
else { else {
event.preventDefault() event.preventDefault()
const snap = !event.shiftKey const snap = !event.shiftKey
addWaypoint.call(ruler, ruler.destination, snap) ruler.dragRulerAddWaypoint(ruler.destination, snap);
} }
} }
return true return true
} }
function onRulerMoveToken(event) {
// This function is invoked by left clicking
if (!this.isDragRuler)
return false
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
const snap = !event.shiftKey
addWaypoint.call(this, this.destination, snap)
}
else
deleteWaypoint()
return true
}
function addWaypoint(point, snap=true) {
if (snap)
point = getSnapPointForToken(point.x, point.y, this.draggedToken)
this.waypoints.push(new PIXI.Point(point.x, point.y));
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
function deleteWaypoint() {
const ruler = canvas.controls.ruler
if (ruler.waypoints.length > 1) {
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)
const rulerOffset = ruler.rulerOffset
ruler._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y})
game.user.broadcastActivity({ruler: ruler})
}
else {
const token = ruler.draggedToken
ruler._endMeasurement()
// Deactivate the drag workflow in mouse
token.mouseInteractionManager._deactivateDragEvents();
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
// This will cancel the current drag operation
// Pass in a fake event that hopefully is enough to allow other modules to function
token._onDragLeftCancel({preventDefault: () => {return}})
}
}
export function getColorForDistance(startDistance, subDistance=0) { export function getColorForDistance(startDistance, subDistance=0) {
if (!this.isDragRuler) if (!this.isDragRuler)
return this.color return this.color
+74
View File
@@ -0,0 +1,74 @@
import {measureDistances} from "./compatibility.js";
import {updateCombatantDragRulerFlags} from "./socket.js";
import {getTokenShape} from "./util.js";
function initTrackingFlag(combatant) {
const initialFlag = {passedWaypoints: [], trackedRound: 0};
let dragRulerFlag = combatant.flags?.dragRuler;
if (dragRulerFlag) {
if (isNaN(dragRulerFlag.trackedRound)) {
mergeObject(dragRulerFlag, initialFlag);
}
}
else {
combatant.flags.dragRuler = initialFlag;
}
}
function getInitializedCombatant(token, combat) {
const combatant = combat.getCombatantByToken(token.data._id);
if (!combatant)
return undefined;
initTrackingFlag(combatant);
return combatant;
}
export async function trackRays(token, rays) {
// Only track movement if the current token is participating in the active combat
const combat = game.combat;
if (!combat)
return;
if (!combat.started)
return;
const combatant = getInitializedCombatant(token, combat);
if (!combatant)
return;
// Check if we have entered a new round. If so, remove the currently stored path
if (combat.data.round > combatant.flags.dragRuler.trackedRound) {
combatant.flags.dragRuler.passedWaypoints = [];
combatant.flags.dragRuler.trackedRound = combat.data.round;
}
// Add the passed waypoints to the combatant
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
const dragRulerFlags = combatant.flags.dragRuler;
const waypoints = dragRulerFlags.passedWaypoints;
for (const ray of rays) {
// Ignore rays that have the same start and end coordinates
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
if (terrainRulerAvailable) {
measureDistances([{ray}], token, getTokenShape(token), true, {terrainRulerInitialState: dragRulerFlags.rulerState});
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
dragRulerFlags.rulerState = ray.terrainRulerFinalState;
}
waypoints.push(ray.A);
}
}
await updateCombatantDragRulerFlags(combat, combatant, dragRulerFlags);
}
export function getMovementHistory(token) {
const combat = game.combat;
if (!combat)
return [];
const combatant = combat.getCombatantByToken(token.data._id);
if (!combatant)
return [];
const dragRulerFlags = combatant.flags.dragRuler;
if (!dragRulerFlags)
return [];
if (combat.data.round > dragRulerFlags.trackedRound)
return [];
return dragRulerFlags.passedWaypoints ?? [];
}
+111
View File
@@ -0,0 +1,111 @@
import {measure} from "./foundry_imports.js"
import {settingsKey} from "./settings.js";
import {getSnapPointForToken} from "./util.js";
export class DragRulerRuler extends Ruler {
// Functions below are overridden versions of functions in Ruler
constructor(user, {color=null}={}) {
super(user, {color});
this.previousWaypoints = [];
this.previousLabels = this.addChild(new PIXI.Container());
}
clear() {
super.clear();
this.previousWaypoints = [];
this.previousLabels.removeChildren().forEach(c => c.destroy());
}
async moveToken(event) {
// This function is invoked by left clicking
if (!this.isDragRuler)
return await super.moveToken(event);
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
const snap = !event.shiftKey;
this.dragRulerAddWaypoint(this.destination, snap);
}
else {
this.dragRulerDeleteWaypoint();
}
}
toJSON() {
const json = super.toJSON();
if (this.draggedToken)
json["draggedToken"] = this.draggedToken.data._id;
return json;
}
update(data) {
// Don't show a GMs drag ruler to non GM players
if (data.draggedToken && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
return;
if (data.draggedToken) {
this.draggedToken = canvas.tokens.get(data.draggedToken);
}
super.update(data);
}
measure(destination, options={}) {
if (this.isDragRuler) {
return measure.call(this, destination, options);
}
else {
return super.measure(destination, options);
}
}
_endMeasurement() {
super._endMeasurement();
this.draggedToken = null;
}
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
dragRulerAddWaypoint(point, snap=true) {
if (snap)
point = getSnapPointForToken(point.x, point.y, this.draggedToken);
this.waypoints.push(new PIXI.Point(point.x, point.y));
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
dragRulerAddWaypointHistory(waypoints) {
waypoints.forEach(waypoint => waypoint.isPrevious = true);
this.waypoints = this.waypoints.concat(waypoints);
for (const waypoint of waypoints) {
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
}
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}) {
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
event.preventDefault();
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
const rulerOffset = this.rulerOffset;
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y});
game.user.broadcastActivity({ruler: this});
}
else {
const token = this.draggedToken;
this._endMeasurement();
// Deactivate the drag workflow in mouse
token.mouseInteractionManager._deactivateDragEvents();
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
// This will cancel the current drag operation
// Pass in a fake event that hopefully is enough to allow other modules to function
token._onDragLeftCancel(event);
}
}
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
if ( destination )
waypoints = waypoints.concat([destination]);
return waypoints.slice(1).map((wp, i) => {
const ray = new Ray(waypoints[i], wp);
ray.isPrevious = Boolean(waypoints[i].isPrevious);
return ray;
});
}
}
+21
View File
@@ -38,6 +38,27 @@ export function registerSettings() {
default: true, default: true,
}) })
game.settings.register(settingsKey, "enableMovementHistory", {
name: "drag-ruler.settings.enableMovementHistory.name",
hint: "drag-ruler.settings.enableMovementHistory.hint",
scope: "world",
config: true,
type: Boolean,
default: true,
});
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
config: false,
type: Number,
default: 0,
});
game.settings.register(settingsKey, "neverShowTerrainRulerHint", {
config: false,
type: Boolean,
default: false,
});
// This setting will be modified by the api if modules register to it // This setting will be modified by the api if modules register to it
game.settings.register(settingsKey, "speedProvider", { game.settings.register(settingsKey, "speedProvider", {
scope: "world", scope: "world",
+17
View File
@@ -0,0 +1,17 @@
let socket;
Hooks.once("socketlib.ready", () => {
socket = socketlib.registerModule("drag-ruler");
socket.register("updateCombatantDragRulerFlags", _socketUpdateCombatantDragRulerFlags);
});
export async function updateCombatantDragRulerFlags(combat, combatant, flags) {
const combatId = combat.id;
const combatantId = combatant._id;
return socket.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, combatantId, flags);
}
async function _socketUpdateCombatantDragRulerFlags(combatId, combatantId, flags) {
const combat = game.combats.get(combatId);
await combat.updateEmbeddedEntity("Combatant", {_id: combatantId, flags: {dragRuler: flags}}, {diff: false});
}
+1 -1
View File
@@ -67,7 +67,7 @@ export class SpeedProvider {
*/ */
getCostForStep(token, area) { getCostForStep(token, area) {
// Lookup the cost for each square occupied by the token // Lookup the cost for each square occupied by the token
const costs = area.map(space => canvas.terrain.costGrid[space.y]?.[space.x]?.multiple ?? 1) const costs = area.map(space => terrainRuler.getCost(space.x, space.y, {token}));
// Return the maximum of the costs // Return the maximum of the costs
return costs.reduce((max, current) => Math.max(max, current)) return costs.reduce((max, current) => Math.max(max, current))
} }
+22 -3
View File
@@ -7,6 +7,9 @@ export function* zip(it1, it2) {
} }
export function getSnapPointForToken(x, y, token) { export function getSnapPointForToken(x, y, token) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return new PIXI.Point(x, y);
}
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) { if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
if (token.getFlag("hex-size-support", "borderSize") % 2 === 0) { if (token.getFlag("hex-size-support", "borderSize") % 2 === 0) {
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid) const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
@@ -16,18 +19,34 @@ export function getSnapPointForToken(x, y, token) {
return new PIXI.Point(...canvas.grid.getCenter(x, y)) return new PIXI.Point(...canvas.grid.getCenter(x, y))
} }
} }
if (canvas.grid.isHex || token.data.width % 2 === 1) { // Tiny tokens can snap to the cells corners
if (!canvas.grid.isHex && token.data.width <= 0.5) {
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
const offsetX = x - topLeftX;
const offsetY = y - topLeftY;
const subGridWidth = Math.floor(canvas.grid.w / 2);
const subGridHeight = Math.floor(canvas.grid.h / 2);
const subGridPosX = Math.floor(offsetX / subGridWidth);
const subGridPosY = Math.floor(offsetY / subGridHeight);
return new PIXI.Point(topLeftX + (subGridPosX + 0.5) * subGridWidth, topLeftY + (subGridPosY + 0.5) * subGridHeight);
}
// Hex tokens, tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
if (canvas.grid.isHex || Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
return new PIXI.Point(...canvas.grid.getCenter(x, y)) return new PIXI.Point(...canvas.grid.getCenter(x, y))
} }
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
const [snappedX, snappedY] = canvas.grid.getCenter(x - canvas.grid.w / 2, y - canvas.grid.h / 2) const [snappedX, snappedY] = canvas.grid.getCenter(x - canvas.grid.w / 2, y - canvas.grid.h / 2)
return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2) return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2)
} }
export function highlightTokenShape(position, shape, color) { export function highlightTokenShape(position, shape, color, alpha) {
const layer = canvas.grid.highlightLayers[this.name];
if ( !layer )
return false;
const area = getAreaFromPositionAndShape(position, shape); const area = getAreaFromPositionAndShape(position, shape);
for (const space of area) { for (const space of area) {
const [x, y] = getPixelsFromGridPosition(space.x, space.y); const [x, y] = getPixelsFromGridPosition(space.x, space.y);
canvas.grid.highlightPosition(this.name, {x, y, color}) canvas.grid.grid.highlightGridPosition(layer, {x, y, color, alpha: 0.25 * alpha});
} }
} }