Skip to content

Commit

Permalink
Merge branch 'release/0.8.4' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
caewok committed Feb 7, 2024
2 parents e8f75f8 + 367e331 commit 8a991f4
Show file tree
Hide file tree
Showing 11 changed files with 723 additions and 339 deletions.
12 changes: 12 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 0.8.4
Improve path cleaning algorithm to remove multiple straight-line points. Closes issue #40.
Refactor measurement of distances and move distances to better account for 3d distance. Closes issue #41.
Remove animation easing for intermediate segments while keeping easing-in for the first segment and easing-out for the last segment.
Add `CONFIG` settings to change the unicode symbol displayed when the ruler is over terrain (or tokens, if tokens count as difficult terrain).
- `CONFIG.elevationruler.SPEED.terrainSymbol`: You can use any text string here. Paste in a unicode symbol if you want a different symbol. For Font Awesome icons, use, e.g., "\uf0e7".
- `CONFIG.elevationruler.SPEED.useFontAwesome`: Set to true to interpet the `terrainSymbol` as FA unicode.
Update geometry lib to v0.2.15.

## Breaking changes
The added methods `Ruler.measureDistance` and `Ruler.measureMoveDistance` were refactored and now take different parameters.

# 0.8.3
Add setting for pathfinder to avoid all tokens or hostile tokens. Closes issue #37.
Misc. fixes to pathfinding to reduce likelihood of it failing to find a path or finding an incorrect path.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ Finally, the DnD Euclidean rule relies on Pythagorean's Theorem, rounded to the

![Screenshot DnD 5e Euclidean Measurement](https://raw.githubusercontent.com/caewok/fvtt-elevation-ruler/feature/media/media/measurement_dnd_euclidean.jpg)


## Token measurement

If you drag the ruler over a token that has been elevated or lowered, the ruler will reflect the elevation of that token (plus or minus manually incremented values). (This does not happen if you are dragging tokens; you must use the ruler tool.)
Expand All @@ -95,6 +94,10 @@ As with the normal Foundry ruler, if you begin a measurement at your token, you

You can modify the system attributes used for walk/fly/burrow as well as the colors used in `CONFIG.elevationruler.SPEED`. You can modify the Token HUD icons in `CONFIG.elevationruler.MOVEMENT_BUTTONS`.

You can modify the icon used when hovering over difficult terrain:
- `CONFIG.elevationruler.SPEED.terrainSymbol`: You can use any text string here. Paste in a unicode symbol if you want a different symbol. For Font Awesome icons, use, e.g., "\uf0e7". (This is the code for [FA lightning bolt](https://fontawesome.com/icons/bolt?f=classic&s=solid).)
- `CONFIG.elevationruler.SPEED.useFontAwesome`: Set to true to interpet the `terrainSymbol` as FA unicode.

Elevation Ruler adds a token property to get the token movement type: `_token.movementType`. You may also want the enumerated movement types: `game.modules.get("elevationruler").api.MOVEMENT_TYPES`.

Elevation Ruler adds token properties to track the last movement made by the token:
Expand Down
2 changes: 1 addition & 1 deletion scripts/BaseGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ PATCHES.BASIC = {};
* @param {Token} token The token placeable being moved.
*/
function _getRulerDestination(wrapped, ray, offset, token) {
if ( canvas.controls.ruler._unsnap ) return ray.B.add(offset);
if ( canvas.controls.ruler._unsnap || ray.pathfinding ) return ray.B.add(offset);

// We are moving from the token center, so add back 1/2 width/height to offset.
if ( !canvas.controls.ruler._unsnappedOrigin ) {
Expand Down
168 changes: 56 additions & 112 deletions scripts/Ruler.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* globals
canvas,
CONFIG
CONST,
duplicate,
game,
getProperty,
PIXI,
Ruler,
ui
*/
/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */
Expand All @@ -18,6 +17,7 @@ PATCHES.SPEED_HIGHLIGHTING = {};
import { SPEED, MODULE_ID } from "./const.js";
import { Settings } from "./settings.js";
import { Ray3d } from "./geometry/3d/Ray3d.js";
import { Point3d } from "./geometry/3d/Point3d.js";
import {
elevationAtWaypoint,
originElevation,
Expand All @@ -36,14 +36,12 @@ import {

import {
tokenIsSnapped,
iterateGridUnderLine,
squareGridShape,
hexGridShape,
log } from "./util.js";

import {
measureDistance,
measureMoveDistance
measureMoveDistance,
gridShapeFromGridCoordinates
} from "./measure_distance.js";

/**
Expand Down Expand Up @@ -211,7 +209,12 @@ function _getMeasurementDestination(wrapped, destination) {
* Add additional controlled tokens to the move, if permitted.
*/
async function _animateMovement(wrapped, token) {
if ( !this.segments.length ) return; // Ruler._animateMovement expects at least one segment.
if ( !this.segments || !this.segments.length ) return; // Ruler._animateMovement expects at least one segment.

log(`Moving ${token.name} ${this.segments.length} segments.`, [...this.segments]);

this.segments.forEach((s, idx) => s.idx = idx);

const promises = [wrapped(token)];
for ( const controlledToken of canvas.tokens.controlled ) {
if ( controlledToken === token ) continue;
Expand Down Expand Up @@ -250,7 +253,6 @@ function _computeDistance(gridSpaces) {
// Determine the distance of each segment.
_computeSegmentDistances.call(this, gridSpaces);
if ( Settings.get(Settings.KEYS.TOKEN_RULER.SPEED_HIGHLIGHTING) ) _computeTokenSpeed.call(this, gridSpaces);
if ( this.segments.length ) this.segments.at(-1).last = true;

// Debugging
if ( this.segments.some(s => !s) ) console.error("Segment is undefined.");
Expand All @@ -276,20 +278,28 @@ function _computeDistance(gridSpaces) {
*/
function _computeSegmentDistances(gridSpaces) {
const token = this._getMovementToken();
const gridless = !gridSpaces;
const gridless = !gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS;
const measureMoveDistance = this.constructor.measureMoveDistance;

// Loop over each segment in turn, adding the physical distance and the move distance.
let totalDistance = 0;
let totalMoveDistance = 0;
if ( this.segments.length ) {
this.segments[0].first = true;
this.segments.at(-1).last = true;
}
for ( const segment of this.segments ) {
const { distance, moveDistance } = measureMoveDistance(segment.ray.A, segment.ray.B, token, gridless);
const { distance, moveDistance } = measureMoveDistance(
segment.ray.A,
segment.ray.B,
token,
{ gridless, useAllElevation: segment.last });
segment.distance = distance;
segment.moveDistance = moveDistance;
totalDistance += segment.distance;
totalMoveDistance += segment.moveDistance;
segment.last = false;
}

this.totalDistance = totalDistance;
this.totalMoveDistance = totalMoveDistance;
}
Expand All @@ -304,7 +314,7 @@ function _computeTokenSpeed(gridSpaces) {
if ( !tokenSpeed ) return;

// Other constants
const gridless = !gridSpaces;
const gridless = !gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS;
const walkDistance = tokenSpeed;
const dashDistance = tokenSpeed * SPEED.MULTIPLIER;

Expand Down Expand Up @@ -399,111 +409,45 @@ function _computeTokenSpeed(gridSpaces) {
*/
function splitSegment(segment, splitMoveDistance, token, gridless) {
if ( splitMoveDistance <= 0 ) return [];
if ( splitMoveDistance > segment.moveDistance ) return [segment];
if ( !segment.moveDistance ) return [segment]; // Segment is length 0 for the move.

// Determine where on the segment ray the cutoff occurs.
// Use canvas grid distance measurements to handle 5-5-5, 5-10-5, other measurement configs.
// At this point, the segment is too long for the cutoff.
// If we are using a grid, split the segment at grid/square hex.
// Find where the segment intersects the last grid square/hex before the cutoff.
const rulerClass = CONFIG.Canvas.rulerClass;
if ( !segment.moveDistance || splitMoveDistance > segment.moveDistance ) return [segment];

// Attempt to move the split distance and determine the split location.
const { A, B } = segment.ray;
gridless ||= (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS);
const breakPoint = gridless
? findGridlessBreakpoint(segment, splitMoveDistance, token)
: findGriddedBreakpoint(segment, splitMoveDistance, token);
const res = Ruler.measureMoveDistance(A, B, token,
{ gridless, useAllElevation: false, stopTarget: splitMoveDistance });

let breakPoint;
if ( gridless ) breakPoint = res.endPoint; // We can get the exact split point.
else {
// We can get the end grid.
// Use halfway between the intersection points for this grid shape.
const gridShape = gridShapeFromGridCoordinates(res.endGridCoords);
const ixs = gridShape.segmentIntersections(A, B);
if ( !ixs || ixs.length === 0 ) breakPoint = A;
else if ( ixs.length === 1 ) breakPoint = B;
else {
breakPoint = Point3d.midPoint(ixs[0], ixs[1]);
breakPoint.z = res.endElevationZ;
}
}

if ( breakPoint.almostEqual(B) ) return [segment];
if ( breakPoint.almostEqual(A) ) return [];

// Split the segment into two at the break point.
const newSegments = [];
for ( const [start, end] of [[A, breakPoint], [breakPoint, B]]) {
const s = {...segment};
newSegments.push(s);
s.ray = new Ray3d(start, end);
const { distance, moveDistance } = rulerClass.measureMoveDistance(start, end, token, gridless);
s.distance = distance;
s.moveDistance = moveDistance;
}
return newSegments;
}

/**
* Search for the best point at which to split a segment on a gridless Canvas so that
* the first half of the segment is splitMoveDistance.
* @param {RulerMeasurementSegment} segment Segment, with ray property, to split
* @param {number} splitMoveDistance Distance, in grid units, of the desired first subsegment move distance
* @param {Token} token Token to use when measuring move distance
* @returns {PIXI.Point}
*/
function findGridlessBreakpoint(segment, splitMoveDistance, token) {
// Binary search to find a reasonably close t value for the split move distance.
// Because the move distance can vary depending on terrain.
const { A, B } = segment.ray;
const MAX_ITER = 20;
let t = splitMoveDistance / segment.moveDistance;
let maxHigh = 1;
let maxLow = 0;
let testSplitPoint;
for ( let i = 0; i < MAX_ITER; i += 1 ) {
testSplitPoint = A.projectToward(B, t);
const { moveDistance } = measureMoveDistance(A, testSplitPoint, token, true);

// Adjust t by half the distance to the max/min t value.
// Need not be all that exact but must be over the target distance.
if ( moveDistance.almostEqual(splitMoveDistance, .01) ) break;
if ( moveDistance > splitMoveDistance ) {
maxHigh = t;
t -= ((t - maxLow) * 0.5);
} else {
maxLow = t;
t += ((maxHigh - t) * 0.5);

}
}
return testSplitPoint;
}

/**
* Search for the best point at which to split a segment on a gridded Canvas so that
* the first half of the segment is splitMoveDistance.
* @param {RulerMeasurementSegment} segment Segment, with ray property, to split
* @param {number} splitMoveDistance Distance, in grid units, of the desired first subsegment move distance
* @param {Token} token Token to use when measuring move distance
* @returns {PIXI.Point}
*/
function findGriddedBreakpoint(segment, splitMoveDistance, token) {
const { A, B } = segment.ray;
const gridShapeFn = canvas.grid.type === CONST.GRID_TYPES.SQUARE ? squareGridShape : hexGridShape;
const rulerClass = CONFIG.Canvas.rulerClass;

// Determine all the grid positions, and add each in turn.
const gridIter = iterateGridUnderLine(A, B);
let breakPoint = A;
for ( const [r1, c1] of gridIter ) {
const [x, y] = canvas.grid.grid.getPixelsFromGridPosition(r1, c1);
const shape = gridShapeFn({x, y});
const ixs = shape
.segmentIntersections(A, B)
.map(ix => PIXI.Point.fromObject(ix));
if ( !ixs.length ) continue;

// Find the midpoint so we avoid the grid edges.
const testBreakPoint = ixs.length === 1
? PIXI.Point.midPoint(breakPoint, ixs[0])
: PIXI.Point.midPoint(ixs[0], ixs[1]);

// Calculate the distance from A to the current test breakpoint.
// If we have exceeded move distance, we are done.
const { moveDistance } = rulerClass.measureMoveDistance(A, testBreakPoint, token, false);
if ( moveDistance > splitMoveDistance ) break;

// Cycle to next iteration.
breakPoint = testBreakPoint;
}
return breakPoint;
const s0 = {...segment};
s0.ray = new Ray3d(A, breakPoint);
s0.distance = res.distance;
s0.moveDistance = res.moveDistance;

const s1 = {...segment};
s1.ray = new Ray3d(breakPoint, B);
s1.distance = segment.distance - res.distance;
s1.moveDistance = segment.moveDistance - res.moveDistance;

if ( segment.first ) { s1.first = false; }
if ( segment.last ) { s0.last = false; }
return [s0, s1];
}

// ----- NOTE: Event handling ----- //
Expand Down Expand Up @@ -670,7 +614,7 @@ PATCHES.BASIC.STATIC_METHODS = {
/**
* Helper to add elevation increments to waypoint
*/
function addWaypointElevationIncrements(ruler, point) {
function addWaypointElevationIncrements(ruler, _point) {
const ln = ruler.waypoints.length;
const newWaypoint = ruler.waypoints[ln - 1];

Expand Down
38 changes: 32 additions & 6 deletions scripts/Token.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* globals
canvas
canvas,
CanvasAnimation,
game,
Ruler
*/
/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */

Expand All @@ -9,6 +12,7 @@ import { Settings } from "./settings.js";
export const PATCHES = {};
PATCHES.TOKEN_RULER = {}; // Assume this patch is only present if the token ruler setting is enabled.
PATCHES.MOVEMENT_TRACKING = {};
PATCHES.PATHFINDING = {};

/**
* Wrap Token.prototype._onDragLeftStart
Expand Down Expand Up @@ -78,7 +82,7 @@ async function _onDragLeftDrop(wrapped, event) {
*/
function lastMoveDistance() {
if ( game.combat?.active && this._lastCombatRoundMove < game.combat.round ) return 0;
return this._lastMoveDistance ?? 0
return this._lastMoveDistance ?? 0;
}

/**
Expand All @@ -90,23 +94,45 @@ function lastMoveDistance() {
*/
function updateToken(document, changes, _options, _userId) {
const token = document.object;
if ( token.isPreview ||
!(Object.hasOwn(changes, "x")
|| Object.hasOwn(changes, "y")
|| Object.hasOwn(changes, "elevation")) ) return;
if ( token.isPreview
|| !(Object.hasOwn(changes, "x")|| Object.hasOwn(changes, "y") || Object.hasOwn(changes, "elevation")) ) return;

if ( game.combat?.active ) token._lastCombatRoundMove = game.combat.round;
const ruler = canvas.controls.ruler;
if ( ruler.active && ruler._getMovementToken() === token ) token._lastMoveDistance = ruler.totalMoveDistance;
else token._lastMoveDistance = Ruler.measureMoveDistance(token.position, token.document, token).moveDistance;
}

/**
* Wrap Token.prototype._onUpdate to remove easing for pathfinding segments.
*/
function _onUpdate(wrapped, data, options, userId) {
if ( options.rulerSegment && options.animation.easing ) {
options.animation.easing = options.firstRulerSegment ? noEndEase(options.animation.easing)
: options.lastRulerSegment ? noStartEase(options.animation.easing)
: undefined;
}
return wrapped(data, options, userId);
}

function noStartEase(easing) {
if ( typeof easing === "string" ) easing = CanvasAnimation[easing];
return pt => (pt < 0.5) ? pt : easing(pt);
}

function noEndEase(easing) {
if ( typeof easing === "string" ) easing = CanvasAnimation[easing];
return pt => (pt > 0.5) ? pt : easing(pt);
}

PATCHES.TOKEN_RULER.WRAPS = {
_onDragLeftStart,
_onDragLeftMove,
_onDragLeftCancel
};

PATCHES.PATHFINDING.WRAPS = { _onUpdate };

PATCHES.TOKEN_RULER.MIXES = { _onDragLeftDrop };

PATCHES.MOVEMENT_TRACKING.HOOKS = { updateToken };
Expand Down
6 changes: 5 additions & 1 deletion scripts/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ export const SPEED = {
WALK: Color.from(0x00ff00),
DASH: Color.from(0xffff00),
MAXIMUM: Color.from(0xff0000)
}
},

// Use Font Awesome font unicode instead of basic unicode for displaying terrain symbol.
useFontAwesome: false, // Set to true to use Font Awesome unicode
terrainSymbol: "🥾" // For Font Awesome, https://fontawesome.com/icons/bolt?f=classic&s=solid would be "\uf0e7".
};

// Add the inversions for lookup
Expand Down
2 changes: 1 addition & 1 deletion scripts/geometry
Loading

0 comments on commit 8a991f4

Please sign in to comment.