Compare commits

..

14 Commits

Author SHA1 Message Date
Manuel Vögele 0456fb0849 Release v1.7.5 2021-06-22 23:20:36 +02:00
Manuel Vögele 01d01f9887 Movement speeds can be floats (fixes #85) 2021-06-22 23:14:53 +02:00
Manuel Vögele 9042b79967 Release v1.7.4 2021-06-18 13:58:54 +02:00
Manuel Vögele 4159d20e18 Verified to work with Foundry 0.8.7 2021-06-18 13:39:10 +02:00
Manuel Vögele 3897577756 Allow the "Monk's Active Tile Triggers" module to interrupt a token movement early 2021-06-18 13:37:26 +02:00
irbian 5c29f401d6 Support D&D 3.5 (#76) 2021-05-27 01:09:12 +02:00
Manuel Vögele 1328d52f94 Send the info whether the dragged entity is a token or a template to the other players (fixes a snapping issue in other players clients) 2021-05-26 16:17:23 +02:00
Manuel Vögele eef05553c0 Release v1.7.3 2021-05-22 22:00:46 +02:00
Manuel Vögele 7ba89e4229 Update code for Foundry 0.8.5 2021-05-22 21:57:59 +02:00
Manuel Vögele d11d6385e2 Release v1.7.2 2021-05-21 14:43:13 +02:00
Manuel Vögele 4d2e4b7715 Calculate the correct snappig for measured templates when placing a waypoint 2021-05-21 14:41:33 +02:00
Manuel Vögele 3cbe41e2be If a measurement is being skipped because of the ruler's rate limiting, schedule the measurement for later to ensure the ruler sticks to the token 2021-05-21 14:38:54 +02:00
Manuel Vögele 732802c579 Fix typo in readme 2021-05-20 11:41:19 +02:00
Manuel Vögele 8bbb11ddaa Update the readme to document all the features included in 1.7.0 2021-05-19 23:20:12 +02:00
11 changed files with 124 additions and 55 deletions
+25
View File
@@ -1,3 +1,28 @@
## 1.7.5
### Bugfixes
- Decimal speeds (as often used in metric game systems) are no longer being rounded down (thanks to DarKDinDoN for diagnosing this bug)
## 1.7.4
### Bugfixes
- Fixed a bug where the ruler would wrongly snap to the grid center for other players when dragging a measurement template
### Compatibility
- Drag Ruler is now compatiblie with the "Monk's Active Tile Triggers" module
- Drag Ruler's Generic Speed Provider is now aware of good default values for the D&D 3.5 game system
- Drag Ruler is now compatible with Foundry 0.8.7
## 1.7.3
### Compatibility
- Drag Ruler is now compatible with Foundry 0.8.5
## 1.7.2
### Bugfixes
- Fixed a bug that prevented waypoints for measurement templates from snapping to any other point than a grid cell corner (or grid cell center on hex)
- Fixed a bug that could cause the ruler to not end up at the token's center (especially if the token is being moved very quickly and then stopped abruptly)
## 1.7.1
### Bugfixes
- Fixed a bug that prevented players from moving their tokens ([#74](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/74))
+19 -16
View File
@@ -1,32 +1,35 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/staebchenfisch)
# Drag Ruler
This module shows a ruler when you drag a token to infrom you how far you've dragged the token from it's start point. Additionally, if you're using a grid, the spaces the token will travel though will be colored depending on your tokens speed. If you're using a gridless map the ruler color will change to convey this information.
This module shows a ruler when you drag a token or measurement template to infrom you how far you've dragged it from it's start point. Additionally, if you're using a grid, the spaces the token will travel though will be colored depending on your tokens speed. By default three colors are being used: green for spaces that your token can reach by walking normally are colored green, spaces that can only be reached by dashing will be colored red and spaces that cannot be reached with the token's speed will be colored red. If you're using a gridless map the ruler color will change to convey this information.
![Drag Ruler being used on a square grids](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/basic_square.webp)
![Drag Ruler being used on a gridless scene](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/basic_gridless.webp)
![Drag Ruler while dragging a measurement template](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/measurement_template.webp)
## Path color
![Drag Ruler demonstration](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/5177746fbb4edb28b6ba09137247d142af575c47/media/drag_ruler.webp)
## Supports Tokens of all sizes
Terrain ruler has excellent support for tokens of all sizes. The Ruler will always originate from the token's center and will always highlight all the squares that tokens move over. If the [Hex Token Size Support](https://foundryvtt.com/packages/hex-size-support) is installed this is also true for large tokens on hex scenes.
The token has a speed of 30ft. All squares on the path within that range are colored in the players color. If the token is running it can cover double that range (this can be changed in the settings). All Squares on the path that can only be reached while running are colored in yellow. Squares on the path that the token cannot possibly reached at regular speeds are colored red. This coloring behavior can be tweaked in the settings and can be overridden by modules and game systems to provide more granular, game system specific control. For more information on how to do this see the [API](#api) section of this document.
![Drag Ruler being used with a large token on a square grid](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/large_token_square.webp)
![Drag Ruler being used with a large token on a hex grid](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/large_token_hex.webp)
## Waypoints
You can add waypoints to the path by pressing spacebar while you drag the token. To remove a placed waypoint press the right mouse button.
## Difficult Terrain support
**To use support for difficult terrain you must install the [Terrain Ruler](https://foundryvtt.com/packages/terrain-ruler) module**
![Demonstration of Waypoints](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/5177746fbb4edb28b6ba09137247d142af575c47/media/waypoints.webp)
With the Terrain Ruler module installed, Drag Ruler is able to take difficult terrain that was placed down using the [Enhanced Terrain Layer](https://foundryvtt.com/packages/enhanced-terrain-layer) or [TerrainLayer](https://foundryvtt.com/packages/TerrainLayer) module into account.
## Difficult Terrain
**To use support for difficult terrain you must install the [Terrain Ruler module](https://foundryvtt.com/packages/terrain-ruler)**
With the Terrain Ruler module installed, Drag Ruler is able to take difficult terrain that was placed with the [TerrainLayer module](https://foundryvtt.com/packages/TerrainLayer/) into account when measuring distances.
![Demonstration of Difficult Terrain support](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/bb8ac5d1acb9d6374d06f7e9c8e2c2dd61192d56/media/terrain_layer.webp)
![Drag Ruler being used to measure distances over difficult terrain on square grid](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/difficult_terrain_square.webp)
![Drag Ruler being used to measure distances over difficult terrain on gridless scenes](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/difficult_terrain_gridless.webp)
## Why would I want to use this instead of ShowDragDistance?
ShowDragDistance isn't maintained anymore. This means that it is at risk to stop working with every foundry update. In fact this process has already begun. As of Foundry Version 0.7.9 ShowDragDistance doesn't work anymore on gridless maps. Drag Ruler on the other hand is fully compatible with the current Foundry release and I'll continue updating it for future foundry releases for the forseeable future.
## Movement history (optional)
*This feature can be disabled in the settings if you don't like it*
In addition Drag Ruler provides more flexibility for game systems and modules via it's api to provide an experience that fits the rules of the game system that you are playing best.
During combat, Drag Ruler will remember the path a token has taken during it's turn. When the token is being picked up again, Drag Ruler will continue measuring where it has left off. The path of the previous movement will be dipslayed in a faded color.
![Demonstration of the Movement History](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/movement_history.webp)
## Game systems with Drag Ruler integration
+4 -4
View File
@@ -2,9 +2,9 @@
"name": "drag-ruler",
"title": "Drag Ruler",
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
"version": "1.7.1",
"minimumCoreVersion" : "0.7.9",
"compatibleCoreVersion" : "0.7.9",
"version": "1.7.5",
"minimumCoreVersion" : "0.8.5",
"compatibleCoreVersion" : "0.8.7",
"authors": [
{
"name": "Manuel Vögele",
@@ -49,7 +49,7 @@
],
"socket": true,
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.7.1.zip",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.7.5.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",
+34 -9
View File
@@ -4,7 +4,7 @@ import {Line} from "./geometry.js";
import {getColorForDistance} from "./main.js"
import {trackRays} from "./movement_tracking.js"
import {recalculate} from "./socket.js";
import {applyTokenSizeOffset, getSnapPointForMeasuredTemplate, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
import {applyTokenSizeOffset, getSnapPointForEntity, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
// This is a modified version of Ruler.moveToken from foundry 0.7.9
export async function moveEntities(draggedEntity, selectedEntities) {
@@ -16,6 +16,9 @@ export async function moveEntities(draggedEntity, selectedEntities) {
if (!this.visible || !this.destination) return false;
if (!draggedEntity) return;
// Wait until all scheduled measurements are done
await this.deferredMeasurementPromise;
// Get the movement rays and check collision along each Ray
// These rays are center-to-center for the purposes of collision checking
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
@@ -69,6 +72,10 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
const isToken = draggedEntity instanceof Token;
const animate = isToken && !game.keyboard.isDown("Alt");
const startWaypoint = animate ? 0 : entityAnimationData[0].rays.length - 1;
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
this.cancelMovement = false;
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) {
if (!wasPaused && game.paused) break;
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
@@ -80,9 +87,15 @@ 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.updateEmbeddedEntity(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)));
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
if (this.cancelMovement) {
entityAnimationData.forEach(ead => ead.rays = ead.rays.slice(0, i + 1));
break;
}
}
if (isToken)
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
@@ -103,8 +116,6 @@ export function onMouseMove(event) {
if (this._state === Ruler.STATES.MOVING) return;
// Extract event data
const mt = event._measureTime || 0;
const originalEvent = event.data.originalEvent;
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
// Hide any existing Token HUD
@@ -112,10 +123,27 @@ export function onMouseMove(event) {
delete event.data.hudState;
// Draw measurement updates
if (Date.now() - mt > 50) {
scheduleMeasurement.call(this, destination, event);
}
function scheduleMeasurement(destination, event) {
const measurementInterval = 50;
const mt = event._measureTime || 0;
const originalEvent = event.data.originalEvent;
if (Date.now() - mt > measurementInterval) {
this.measure(destination, {snap: !originalEvent.shiftKey});
event._measureTime = Date.now();
this._state = Ruler.STATES.MEASURING;
window.clearTimeout(this.deferredMeasurementTimeout);
this.deferredMeasurementTimeout = undefined;
this.deferredMeasurementResolve?.();
}
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);
}
}
}
@@ -126,10 +154,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
return []
if (snap) {
if (isToken)
destination = getSnapPointForToken(destination.x, destination.y, this.draggedEntity);
else
destination = getSnapPointForMeasuredTemplate(destination.x, destination.y);
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
}
const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS);
+1 -2
View File
@@ -50,7 +50,7 @@ Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
const entry = {
name: "drag-ruler.resetMovementHistory",
icon: '<i class="fas fa-undo-alt"></i>',
callback: li => resetMovementHistory(ui.combat.combat, li.data('combatant-id')),
callback: li => resetMovementHistory(ui.combat.viewed, li.data('combatant-id')),
};
menu.splice(1, 0, entry);
});
@@ -154,7 +154,6 @@ function onEntityDragLeftDrop(event) {
const ruler = canvas.controls.ruler
if (!ruler.isDragRuler)
return false
onMouseMove.call(ruler, event);
// 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
+12 -12
View File
@@ -4,19 +4,19 @@ import {getTokenShape, zip} from "./util.js";
function initTrackingFlag(combatant) {
const initialFlag = {passedWaypoints: [], trackedRound: 0};
let dragRulerFlag = combatant.flags?.dragRuler;
let dragRulerFlag = combatant.data.flags.dragRuler;
if (dragRulerFlag) {
if (isNaN(dragRulerFlag.trackedRound)) {
mergeObject(dragRulerFlag, initialFlag);
}
}
else {
combatant.flags.dragRuler = initialFlag;
combatant.data.flags.dragRuler = initialFlag;
}
}
function getInitializedCombatant(token, combat) {
const combatant = combat.getCombatantByToken(token.data._id);
const combatant = combat.getCombatantByToken(token.id);
if (!combatant)
return undefined;
initTrackingFlag(combatant);
@@ -43,14 +43,14 @@ function calculateUpdate(combat, token, rays) {
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;
if (combat.data.round > combatant.data.flags.dragRuler.trackedRound) {
combatant.data.flags.dragRuler.passedWaypoints = [];
combatant.data.flags.dragRuler.trackedRound = combat.data.round;
}
// Add the passed waypoints to the combatant
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS);
const dragRulerFlags = combatant.flags.dragRuler;
const dragRulerFlags = combatant.data.flags.dragRuler;
const waypoints = dragRulerFlags.passedWaypoints;
for (const ray of rays) {
// Ignore rays that have the same start and end coordinates
@@ -63,17 +63,17 @@ function calculateUpdate(combat, token, rays) {
waypoints.push(ray.A);
}
}
return {_id: combatant._id, dragRulerFlags};
return {_id: combatant.id, dragRulerFlags};
}
export function getMovementHistory(token) {
const combat = game.combat;
if (!combat)
return [];
const combatant = combat.getCombatantByToken(token.data._id);
const combatant = combat.getCombatantByToken(token.id);
if (!combatant)
return [];
const dragRulerFlags = combatant.flags.dragRuler;
const dragRulerFlags = combatant.data.flags.dragRuler;
if (!dragRulerFlags)
return [];
if (combat.data.round > dragRulerFlags.trackedRound)
@@ -82,8 +82,8 @@ export function getMovementHistory(token) {
}
export async function resetMovementHistory(combat, combatantId) {
const combatant = combat.getCombatant(combatantId);
const dragRulerFlags = combatant.flags.dragRuler;
const combatant = combat.combatants.get(combatantId);
const dragRulerFlags = combatant.data.flags.dragRuler;
if (!dragRulerFlags)
return;
dragRulerFlags.passedWaypoints = null;
+12 -5
View File
@@ -1,7 +1,7 @@
import {measure} from "./foundry_imports.js"
import {getMovementHistory} from "./movement_tracking.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForToken} from "./util.js";
import {getSnapPointForEntity} from "./util.js";
export class DragRulerRuler extends Ruler {
// Functions below are overridden versions of functions in Ruler
@@ -33,8 +33,11 @@ export class DragRulerRuler extends Ruler {
toJSON() {
const json = super.toJSON();
if (this.draggedEntity)
json["draggedEntity"] = this.draggedEntity.data._id;
if (this.draggedEntity) {
const isToken = this.draggedEntity instanceof Token;
json["draggedEntityIsToken"] = isToken;
json["draggedEntity"] = this.draggedEntity.id;
}
return json;
}
@@ -44,7 +47,10 @@ export class DragRulerRuler extends Ruler {
return;
if (data.draggedEntity) {
if (data.draggedEntityIsToken)
this.draggedEntity = canvas.tokens.get(data.draggedEntity);
else
this.draggedEntity = canvas.templates.get(data.draggedEntity);
}
super.update(data);
}
@@ -65,8 +71,9 @@ export class DragRulerRuler extends Ruler {
// 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.draggedEntity);
if (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));
}
+4 -4
View File
@@ -12,7 +12,7 @@ export function updateCombatantDragRulerFlags(combat, updates) {
const combatId = combat.id;
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
return socket.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.getCombatant(update._id).token._id))));
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id))));
}
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
@@ -20,10 +20,10 @@ async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
const combat = game.combats.get(combatId);
const requestedUpdates = updates.length;
updates = updates.filter(update => {
const actor = combat.getCombatant(update._id).actor;
const actor = combat.combatants.get(update._id).actor;
if (!actor)
return false;
return actor.hasPerm(user, "OWNER");
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`);
@@ -31,7 +31,7 @@ async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
updates = updates.map(update => {
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
});
await combat.updateEmbeddedEntity("Combatant", updates, {diff: false});
await combat.updateEmbeddedDocuments("Combatant", updates, {diff: false});
}
export function recalculate(tokens) {
+1 -1
View File
@@ -132,7 +132,7 @@ export class GenericSpeedProvider extends SpeedProvider {
const speedAttribute = this.getSetting("speedAttribute")
if (!speedAttribute)
return []
const tokenSpeed = parseInt(getProperty(token, speedAttribute));
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 []
+2
View File
@@ -8,6 +8,7 @@ export function getDefaultSpeedAttribute() {
case "lancer":
return "actor.data.data.mech.speed"
case "pf1":
case "D35E":
return "actor.data.data.attributes.speed.land.total"
case "sfrpg":
return "actor.data.data.attributes.speed.value";
@@ -27,6 +28,7 @@ export function getDefaultDashMultiplier() {
case "dnd5e":
case "lancer":
case "pf1":
case "D35E":
case "sfrpg":
case "shadowrun5e":
return 2
+8
View File
@@ -62,6 +62,14 @@ export function getSnapPointForMeasuredTemplate(x, y) {
return new PIXI.Point(snappedX, snappedY);
}
export function getSnapPointForEntity(x, y, entity) {
const isToken = entity instanceof Token;
if (isToken)
return getSnapPointForToken(x, y, entity);
else
return getSnapPointForMeasuredTemplate(x, y);
}
export function highlightTokenShape(position, shape, color, alpha) {
const layer = canvas.grid.highlightLayers[this.name];
if ( !layer )