Skip to content

Commit

Permalink
Merge branch 'release/0.10.11' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
caewok committed Sep 17, 2024
2 parents 83b3a6a + 59b059c commit cdb5737
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 395 deletions.
9 changes: 9 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# 0.10.11
When snapping pathfinding to grid, avoid two waypoints within the same space when snapping is possible.
Fix for ray undefined in cost measurement.
Add invisibility to the default ignored statuses for pathfinding. Closes #202.
Don't show movement penalty in the ruler labels if token has certain statuses, like hidden. Closes #200.
Faster measurement of movement penalty through regions in conjunction with improvements added in Terrain Mapper v0.4.6.
Fix for ruler not functioning in sfrpg. Closes #206.
Update libGeometry to v0.3.14.

# 0.10.10
Improve how the pathfinding path is cleaned when snapping to grids to avoid weird backstepping issues. Should be a bit more aggressive in finding a viable grid-center path.
Don't pathfind around hidden tokens. Closes #200.
Expand Down
12 changes: 7 additions & 5 deletions scripts/Ruler.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ function _getMeasurementSegments(wrapped) {
RegionMovementWaypoint3d.fromObject(lastSegment.ray.B)
);


// Determine the region path.
pathPoints.length = 0;
const ElevationHandler = OTHER_MODULES.TERRAIN_MAPPER.API.ElevationHandler;
Expand All @@ -373,12 +372,13 @@ function _getMeasurementSegments(wrapped) {
const flying = movementTypeStart === MOVEMENT_TYPES.FLY || movementTypeEnd === MOVEMENT_TYPES.FLY;
const burrowing = movementTypeStart === MOVEMENT_TYPES.BURROW || movementTypeEnd === MOVEMENT_TYPES.BURROW;
const subPath = ElevationHandler.constructPath(prevPt, nextPt, { flying, burrowing, token });
log(`Subpath ${prevPt.x},${prevPt.y},${prevPt.z} -> ${nextPt.x},${nextPt.y},${nextPt.z}. Flying: ${flying}; burrowing: ${burrowing}.`, subPath);
subPath.shift(); // Remove prevPt from the array.
pathPoints.push(...subPath);
prevPt = nextPt;
}
const t1 = performance.now();
log(`Found terrain path with ${pathPoints.length} points in ${t1-t0} ms.`);
log(`Found terrain path with ${pathPoints.length} points in ${t1-t0} ms.`, initialPath, pathPoints);
}
const t1 = performance.now();
const key = `${lastSegment.ray.A.key}|${lastSegment.ray.B.key}`;
Expand Down Expand Up @@ -495,9 +495,11 @@ function _getCostFunction() {

// Construct a move penalty instance that covers all the segments.
const movePenaltyInstance = this._movePenaltyInstance ??= new MovePenalty(this.token);
const path = this.segments.map(s => GridCoordinates3d.fromObject(s.ray.A));
path.push(GridCoordinates3d.fromObject(this.segments.at(-1).ray.B));
movePenaltyInstance.restrictToPath(path);
if ( this.segments.length ) {
const path = this.segments.map(s => GridCoordinates3d.fromObject(s.ray.A));
path.push(GridCoordinates3d.fromObject(this.segments.at(-1).ray.B));
movePenaltyInstance.restrictToPath(path);
}
return (prevOffset, currOffset, offsetDistance) => {
if ( !(prevOffset instanceof GridCoordinates3d) ) prevOffset = GridCoordinates3d.fromOffset(prevOffset);
if ( !(currOffset instanceof GridCoordinates3d) ) currOffset = GridCoordinates3d.fromOffset(currOffset);
Expand Down
5 changes: 3 additions & 2 deletions scripts/Token.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Ruler
import { MODULE_ID, FLAGS } from "./const.js";
import { Settings } from "./settings.js";
import { log } from "./util.js";
import { GridCoordinates3d } from "./geometry/3d/GridCoordinates3d.js";
import { MovePenalty } from "./measurement/MovePenalty.js";

// Patches for the Token class
export const PATCHES = {};
Expand Down Expand Up @@ -51,7 +51,8 @@ function preUpdateToken(document, changes, _options, _userId) {
} else {
// Some other move; likely arrow keys.
const numPrevDiagonal = game.combat?.started ? (token._combatMoveData?.numDiagonal ?? 0) : 0;
const res = GridCoordinates3d.gridMeasurementForSegment(token.position, token.document._source, numPrevDiagonal);
const mp = new MovePenalty(token);
const res = mp.measureSegment(token.position, token.document._source, { numPrevDiagonal });
lastMoveDistance = res.cost;
numDiagonal = res.numDiagonal;
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/geometry
227 changes: 19 additions & 208 deletions scripts/measurement/Grid.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
/* globals
canvas,
CONFIG,
CONST,
game
*/
/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */
"use strict";

import { GridCoordinates3d } from "../geometry/3d/GridCoordinates3d.js";
import { HexGridCoordinates3d } from "../geometry/3d/HexGridCoordinates3d.js";
import { Point3d } from "../geometry/3d/Point3d.js";
import { Settings } from "../settings.js";

Expand All @@ -25,19 +22,6 @@ PATCHES_GridlessGrid.BASIC = {};
PATCHES_SquareGrid.BASIC = {};
PATCHES_HexagonalGrid.BASIC = {};

// Store the flipped key/values. And lock the keys.
const CHANGE = {
NONE: 0,
V: 1,
H: 2,
D: 3,
E: 4
};
Object.entries(CHANGE).forEach(([key, value]) => CHANGE[value] = key);
Object.freeze(CHANGE);


// ----- NOTE: GridlessGrid ----- //

/**
* Wrap GridlessGrid#getDirectPath
Expand All @@ -51,6 +35,7 @@ function getDirectPathGridless(wrapped, waypoints) {
if ( !(waypoints[0] instanceof Point3d) ) return offsets2d;

// 1-to-1 relationship between the waypoints and the offsets2d for gridless.
const GridCoordinates3d = CONFIG.GeometryLib.threeD.GridCoordinates3d;
return offsets2d.map((offset2d, idx) => {
const offset3d = GridCoordinates3d.fromOffset(offset2d);
const waypoint = GridCoordinates3d.fromObject(waypoints[idx]);
Expand All @@ -59,183 +44,29 @@ function getDirectPathGridless(wrapped, waypoints) {
});
}


// ----- NOTE: SquareGrid ----- //

/**
* Constructs a direct path for a square grid, accounting for elevation and diagonal elevation
* in a quasi-optimal manner. Spreads out the elevation moves over the course of the path.
* Double-diagonals are slightly favored for some diagonal measurement
* types, so this accounts for those by preferring to move elevation when moving 2d diagonally.
* @param {RegionMovementWaypoint3d} start
* @param {RegionMovementWaypoint3d} end
* @param {GridOffset[]} [path2d] Optional path2d for the start and end waypoints.
* @returns {GridCoordinates3d[]}
* Wrap HexagonalGrid#getDirectPath and SquareGrid#getDirectPath
* Returns the sequence of grid offsets of a shortest, direct path passing through the given waypoints.
* @param {Point3d[]} waypoints The waypoints the path must pass through
* @returns {GridOffset[]} The sequence of grid offsets of a shortest, direct path
* @abstract
*/
function directPath3dSquare(start, end) {
start = GridCoordinates3d.fromObject(start);
end = GridCoordinates3d.fromObject(end);
const points = CONFIG.GeometryLib.utils.bresenhamLine3d(start.i, start.j, start.k, end.i, end.j, end.k);
const path3d = [start];
// Convert points to GridCoordinates3d. Start and end repeat; skip.
for ( let i = 3, n = points.length - 3; i < n; i += 3 ) path3d.push(GridCoordinates3d.fromOffset({
i: points[i],
j: points[i + 1],
k: points[i + 2] }));
path3d.push(end);
return path3d;
}
function getDirectPathGridded(wrapped, waypoints) {
const { HexGridCoordinates3d, GridCoordinates3d } = CONFIG.GeometryLib.threeD;

/**
* Construct a function to determine the offset cost for this canvas for a single 3d move on a square grid.
* @param {number} numDiagonals
* @returns {function}
* - @param {GridCoordinates3d} prevOffset
* - @param {GridCoordinates3d} currOffset
* - @returns {number}
*/
function singleOffsetSquareDistanceFn(numDiagonals = 0) {
const diagonals = canvas.grid.diagonals ?? game.settings.get("core", "gridDiagonals");
const D = CONST.GRID_DIAGONALS;
let nDiag = numDiagonals;
let fn;
if ( diagonals === D.ALTERNATING_1 || diagonals === D.ALTERNATING_2 ) {
const kFn = diagonals === D.ALTERNATING_2
? () => nDiag & 1 ? 2 : 1
: () => nDiag & 1 ? 1 : 2;
fn = (prevOffset, currOffset) => {
const isElevationMove = prevOffset.k !== currOffset.k;
const isStraight2dMove = (prevOffset.i === currOffset.i) ^ (prevOffset.j === currOffset.j);
const isDiagonal2dMove = (prevOffset.i !== currOffset.i) && (prevOffset.j !== currOffset.j);
const s = isStraight2dMove || (!isDiagonal2dMove && isElevationMove);
const d1 = isDiagonal2dMove && !isElevationMove;
const d2 = isDiagonal2dMove && isElevationMove;
if ( d1 || d2 ) nDiag++;
const k = kFn();
return (s + (k * d1) + (k * d2)) * canvas.grid.distance;
};
} else {
let k = 1;
let k2 = 1;
switch ( diagonals ) {
case D.EQUIDISTANT: k = 1; k2 = 1; break;
case D.EXACT: k = Math.SQRT2; k2 = Math.SQRT3; break;
case D.APPROXIMATE: k = 1.5; k2 = 1.75; break;
case D.RECTILINEAR: k = 2; k2 = 3; break;
}
fn = (prevOffset, currOffset) => {
const isElevationMove = prevOffset.k !== currOffset.k;
const isStraight2dMove = (prevOffset.i === currOffset.i) ^ (prevOffset.j === currOffset.j);
const isDiagonal2dMove = (prevOffset.i !== currOffset.i) && (prevOffset.j !== currOffset.j);
const s = isStraight2dMove || (!isDiagonal2dMove && isElevationMove);
const d1 = isDiagonal2dMove && !isElevationMove;
const d2 = isDiagonal2dMove && isElevationMove;
return (s + (k * d1) + (k2 * d2)) * canvas.grid.distance;
};
if ( !(waypoints[0] instanceof Point3d) ) return wrapped(waypoints);
let prevWaypoint = GridCoordinates3d.fromObject(waypoints[0]);
const path3d = [];
const path3dFn = canvas.grid.isHexagonal ? HexGridCoordinates3d._directPathHex : GridCoordinates3d._directPathSquare;
for ( let i = 1, n = waypoints.length; i < n; i += 1 ) {
const currWaypoint = GridCoordinates3d.fromObject(waypoints[i]);
const segments3d = path3dFn(prevWaypoint, currWaypoint);
path3d.push(...segments3d);
prevWaypoint = currWaypoint;
}
Object.defineProperty(fn, "diagonals", {
get: () => nDiag
});
return fn;
}

// ----- NOTE: HexagonalGrid ----- //

/**
* Constructs a direct path for a hex grid, accounting for elevation and diagonal elevation.
* Spreads out the elevation moves over the course of the path.
* For a hex grid, there is no "double diagonal" to worry about.
* @param {RegionMovementWaypoint3d} start
* @param {RegionMovementWaypoint3d} end
* @param {GridOffset[]} [path2d] Optional path2d for the start and end waypoints.
* @returns {HexGridCoordinates3d[]}
*/
function directPath3dHex(start, end) {
start = HexGridCoordinates3d.fromObject(start);
end = HexGridCoordinates3d.fromObject(end);
const points = CONFIG.GeometryLib.utils.bresenhamLine4d(
start.q, start.r, start.s, start.k,
end.q, end.r, end.s, end.k);
const path3d = [start];
// Convert points to GridCoordinates3d. Start and end repeat; skip.
for ( let i = 4, n = points.length - 4; i < n; i += 4 ) path3d.push(HexGridCoordinates3d.fromHexCube({
q: points[i],
r: points[i + 1],
s: points[i + 2],
k: points[i + 3] }));
path3d.push(end);
return path3d;
}

/**
* Construct a function to determine the offset cost for this canvas for a single 3d move on a hex grid.
* For hexes, the diagonal only occurs with an elevation + hex move.
* @param {number} numDiagonals
* @returns {function}
* - @param {GridCoordinates3d} prevOffset
* - @param {GridCoordinates3d} currOffset
* - @returns {number}
*/
function singleOffsetHexDistanceFn(numDiagonals = 0) {
const diagonals = canvas.grid.diagonals ?? game.settings.get("core", "gridDiagonals");
const D = CONST.GRID_DIAGONALS;
let nDiag = numDiagonals;
let fn;
if ( diagonals === D.ALTERNATING_1 || diagonals === D.ALTERNATING_2 ) {
const kFn = diagonals === D.ALTERNATING_2
? () => nDiag & 1 ? 2 : 1
: () => nDiag & 1 ? 1 : 2;
fn = (prevOffset, currOffset) => {
// For hex moves, no diagonal 2d. Just diagonal if both elevating and moving in 2d.
const isElevationMove = prevOffset.k !== currOffset.k;
const is2dMove = prevOffset.i !== currOffset.i || prevOffset.j !== currOffset.j;
const s = isElevationMove ^ is2dMove;
const d = !s;
nDiag += d;
const k = kFn();
return (s + (k * d)) * canvas.grid.distance;
};
} else {
let k = 1;
switch ( diagonals ) {
case D.EQUIDISTANT: k = 1; break;
case D.EXACT: k = Math.SQRT2; break;
case D.APPROXIMATE: k = 1.5; break;
case D.RECTILINEAR: k = 2; break;
}
fn = (prevOffset, currOffset) => {
const isElevationMove = prevOffset.k !== currOffset.k;
const is2dMove = prevOffset.i !== currOffset.i || prevOffset.j !== currOffset.j;
const s = isElevationMove ^ is2dMove;
const d = !s;
return (s + (k * d)) * canvas.grid.distance;
};
}
Object.defineProperty(fn, "diagonals", {
get: () => nDiag
});
return fn;
}

/**
* Get the function to measure the offset distance for a given distance with given previous diagonals.
* @param {number} [diagonals=0]
* @returns {function}
*/
export function getOffsetDistanceFn(diagonals = 0) {
let offsetDistanceFn;
switch ( canvas.grid.type ) {
case CONST.GRID_TYPES.GRIDLESS:
offsetDistanceFn = (a, b) => CONFIG.GeometryLib.utils.pixelsToGridUnits(Point3d.distanceBetween(a, b));
break;
case CONST.GRID_TYPES.SQUARE:
offsetDistanceFn = singleOffsetSquareDistanceFn(diagonals);
break;
default: // All hex grids
offsetDistanceFn = singleOffsetHexDistanceFn(diagonals);
}
return offsetDistanceFn;
}

/**
* Measure a path for a gridded scene. Handles hex and square grids.
Expand All @@ -247,6 +78,7 @@ export function getOffsetDistanceFn(diagonals = 0) {
*/
function _measurePath(wrapped, waypoints, { cost }, result) {
if ( !(waypoints[0] instanceof Point3d) ) return wrapped(waypoints, { cost }, result);
const GridCoordinates3d = CONFIG.GeometryLib.threeD.GridCoordinates3d;
initializeResultObject(result);
result.waypoints.forEach(waypoint => initializeResultObject(waypoint));
result.segments.forEach(segment => initializeResultObject(segment));
Expand All @@ -259,7 +91,7 @@ function _measurePath(wrapped, waypoints, { cost }, result) {
// Copy the waypoint so it can be manipulated.
let start = waypoints[0];
cost ??= (prevOffset, currOffset, offsetDistance) => offsetDistance;
const offsetDistanceFn = getOffsetDistanceFn(0); // Diagonals = 0.
const offsetDistanceFn = GridCoordinates3d.getOffsetDistanceFn(0); // Diagonals = 0.
const altGridDistanceFn = GridCoordinates3d.alternatingGridDistanceFn();
let diagonals = canvas.grid.diagonals ?? game.settings.get("core", "gridDiagonals");
const D = GridCoordinates3d.GRID_DIAGONALS;
Expand Down Expand Up @@ -306,27 +138,6 @@ function _measurePath(wrapped, waypoints, { cost }, result) {
return result;
}

/**
* Wrap HexagonalGrid#getDirectPath and SquareGrid#getDirectPath
* Returns the sequence of grid offsets of a shortest, direct path passing through the given waypoints.
* @param {Point3d[]} waypoints The waypoints the path must pass through
* @returns {GridOffset[]} The sequence of grid offsets of a shortest, direct path
* @abstract
*/
function getDirectPathGridded(wrapped, waypoints) {
if ( !(waypoints[0] instanceof Point3d) ) return wrapped(waypoints);
let prevWaypoint = GridCoordinates3d.fromObject(waypoints[0]);
const path3d = [];
const path3dFn = canvas.grid.isHexagonal ? directPath3dHex : directPath3dSquare;
for ( let i = 1, n = waypoints.length; i < n; i += 1 ) {
const currWaypoint = GridCoordinates3d.fromObject(waypoints[i]);
const segments3d = path3dFn(prevWaypoint, currWaypoint);
path3d.push(...segments3d);
prevWaypoint = currWaypoint;
}
return path3d;
}

// ----- NOTE: Patches ----- //

PATCHES_GridlessGrid.BASIC.WRAPS = { getDirectPath: getDirectPathGridless };
Expand Down
Loading

0 comments on commit cdb5737

Please sign in to comment.