Skip to content

Commit

Permalink
Fix using Lancer system-specific function for automatic token elevati…
Browse files Browse the repository at this point in the history
…on change.
  • Loading branch information
Wibble199 committed Feb 28, 2025
1 parent a1f15fa commit 9b01638
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 9 deletions.
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "terrain-height-tools",
"title": "Terrain Height Tools",
"description": "Tools for painting grid cells with terrain heights and calculating line of sight with respect to these heights.",
"version": "0.5.1",
"version": "0.5.2",
"compatibility": {
"minimum": "12",
"verified": "12",
Expand Down
15 changes: 7 additions & 8 deletions module/hooks/token-elevation.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { moduleName, settings } from "../consts.mjs";
import { TerrainHeightLayer } from "../layers/terrain-height-layer.mjs";
import { toSceneUnits } from "../utils/grid-utils.mjs";
import { getSpacesUnderToken, toSceneUnits } from "../utils/grid-utils.mjs";
import { getTerrainType } from "../utils/terrain-types.mjs";

/**
Expand Down Expand Up @@ -61,16 +61,15 @@ export function handleTokenPreCreation(tokenDoc, _createData, _options, userId)
function getHighestTerrainUnderToken(token, position) {
const hm = TerrainHeightLayer.current?._heightMap;

// We may not want to get the cells under the current position. In this case, we need to work out the offset between
// the token's actual position (which is what getOccupiedSpaces returns) and the desired position.
const offset = position
? { x: position.x - token.x, y: position.y - token.y }
: { x: 0, y: 0 };
// If a position has been provided, use that position. Otherwise, use the token's position.
const { x, y } = position ?? token;
const { width, height, hexagonalShape } = token.document;
const { type: gridType, size: gridSize } = canvas.grid;

let highest = 0;

for (const space of token.getOccupiedSpaces()) {
const { i, j } = canvas.grid.getOffset({ x: space.x + offset.x, y: space.y + offset.y });
for (const space of getSpacesUnderToken(x, y, width, height, gridType, gridSize, hexagonalShape)) {
const { i, j } = canvas.grid.getOffset(space);
const terrains = hm.get(i, j);
if (!(terrains?.length > 0)) continue; // no terrain at this cell

Expand Down
218 changes: 218 additions & 0 deletions module/utils/grid-utils.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { cacheReturn } from "./misc-utils.mjs";

/** The side length of a hexagon with a grid size of 1 (apothem of 0.5). */
const HEX_UNIT_SIDE_LENGTH = 1 / Math.sqrt(3);

/**
* Returns a set of coordinates for the grid cell at the given position.
* @param {number} row
Expand Down Expand Up @@ -111,3 +116,216 @@ export function fromSceneUnits(val) {
? val / canvas.scene.dimensions.distance
: null;
}

const getSquareTokenSpaces = cacheReturn(
/**
* Calculates the coordinates of spaces underneath a square token.
* @param {number} width
* @param {number} height
*/
function(width, height) {
/** @type {{ x: number; y: number; }[]} */
const spaces = [];

for (let x = 0; x < width; x++)
for (let y = 0; y < height; y++)
spaces.push({ x: x + 0.5, y: y + 0.5 });

return spaces;
}
);

const getEllipseHexTokenSpaces = cacheReturn(
/**
* Calculates the cube coordinates of all spaces occupied by an ellipse token with the given width/height.
* @param {number} primaryAxisSize Size of the token in the primary direction, measured in cells.
* @param {number} secondaryAxisSize Size of the token in the secondary direction, measured in cells.
* @param {boolean} isColumnar true for hex columns, false for hex rows.
* @param {boolean} isVariant2 false for ELLIPSE_1, true for ELLIPSE_2.
*/
function(primaryAxisSize, secondaryAxisSize, isColumnar, isVariant2) {
// Ellipses require the size in primary axis to be at least as big as `floor(secondaryAxisSize / 2) + 1`.
// E.G. for columnar grids, for a width of 5, the height must be 3 or higher. For a width of 6, height must be
// at least 4 or higher. Same is true for rows, but in the opposite axis.
if (primaryAxisSize < Math.floor(secondaryAxisSize / 2) + 1) {
return [];
}

const secondaryAxisOffset = Math[isVariant2 ? "ceil" : "floor"]((secondaryAxisSize - 1) / 2) * HEX_UNIT_SIDE_LENGTH * 1.5 + HEX_UNIT_SIDE_LENGTH;

/** @type {{ x: number; y: number; }[]} */
const spaces = [];

// Track the offset distance from the largest part of the hex (in primary), and which side we're on.
// The initial side we use (sign) depends on the variant of ellipse we're building.
let offsetDist = 0;
let offsetSign = isVariant2 ? 1 : -1;

for (let i = 0; i < secondaryAxisSize; i++) {
const primaryAxisOffset = (offsetDist + 1) / 2;
const secondaryPosition = offsetDist * offsetSign * HEX_UNIT_SIDE_LENGTH * 1.5 + secondaryAxisOffset;

// The number of spaces in this primary axis decreases by 1 each time the offsetDist increases by 1: at the
// 0 (the largest part of the shape), we have the full primary size number of cells. Either side of this, we
// have primary - 1, either side of those primary - 2, etc.
for (let j = 0; j < primaryAxisSize - offsetDist; j++) {
spaces.push(coordinate(j + primaryAxisOffset, secondaryPosition));
}

// Swap over the offset side, and increase dist if neccessary
offsetSign *= -1;
if (i % 2 === 0) offsetDist++;
}

return spaces;

/**
* @param {number} primary
* @param {number} secondary
*/
function coordinate(primary, secondary) {
return isColumnar ? { x: secondary, y: primary } : { x: primary, y: secondary };
}
}
);

const getTrapezoidHexTokenSpaces = cacheReturn(
/**
* Calculates the cube coordinates of all spaces occupied by an trapezoid token with the given width/height.
* @param {number} primaryAxisSize Size of the token in the primary direction, measured in cells.
* @param {number} secondaryAxisSize Size of the token in the secondary direction, measured in cells.
* @param {boolean} isColumnar true for hex columns, false for hex rows.
* @param {boolean} isVariant2 false for TRAPEZOID_1, true for TRAPEZOID_2.
*/
function(primaryAxisSize, secondaryAxisSize, isColumnar, isVariant2) {
// For trapezoid to work, the size in the primary axis must be equal to or larger than the size in the secondary
if (primaryAxisSize < secondaryAxisSize) {
return [];
}

const secondaryAxisOffset = isVariant2 ? HEX_UNIT_SIDE_LENGTH + (secondaryAxisSize - 1) * HEX_UNIT_SIDE_LENGTH * 1.5 : HEX_UNIT_SIDE_LENGTH;

/** @type {{ x: number; y: number; }[]} */
const spaces = [];

// Trazpezoids are simple. Start with a line in the primary direction that is the full primary size.
// Then, for each cell in the secondary direction, reduce the primary by one.
// If we are doing variant1 we offset in the secondary by one direction, for variant2 we go the other direction.
for (let i = 0; i < secondaryAxisSize; i++) {
const primaryAxisOffset = (i + 1) / 2;
const secondaryPosition = i * (isVariant2 ? -1 : 1) * HEX_UNIT_SIDE_LENGTH * 1.5 + secondaryAxisOffset;

for (let j = 0; j < primaryAxisSize - i; j++) {
spaces.push(coordinate(j + primaryAxisOffset, secondaryPosition));
}
}

return spaces;

/**
* @param {number} primary
* @param {number} secondary
*/
function coordinate(primary, secondary) {
return isColumnar ? { x: secondary, y: primary } : { x: primary, y: secondary };
}
}
);

const getRectangleHexTokenSpaces = cacheReturn(
/**
* Calculates the cube coordinates of all spaces occupied by an trapezoid token with the given width/height.
* @param {number} primaryAxisSize Size of the token in the primary direction, measured in cells.
* @param {number} secondaryAxisSize Size of the token in the secondary direction, measured in cells.
* @param {boolean} isColumnar true for hex columns, false for hex rows.
* @param {boolean} isVariant2 false for TRAPEZOID_1, true for TRAPEZOID_2.
*/
function(primaryAxisSize, secondaryAxisSize, isColumnar, isVariant2) {
// If the size in the primary direction is 1, the size in the secondary direction must be no more than one.
// For primary size >= 2, any size secondary is acceptable.
if (primaryAxisSize === 1 && secondaryAxisOffset > 1) {
return [];
}

/** @param {{ x: number; y: number; }[]} */
const spaces = [];

const largeRemainder = isVariant2 ? 1 : 0;

// Spaces under rectangles are easy. They just alternate size in the primary direction by 0 and -1 as we iterate
// through the cells in the secondary direction.
for (let i = 0; i < secondaryAxisSize; i++) {
const isLarge = i % 2 === largeRemainder;
for (let j = 0; j < primaryAxisSize - (isLarge ? 0 : 1); j++) {
spaces.push(coordinate(
j + (isLarge ? 0.5 : 1),
i * HEX_UNIT_SIDE_LENGTH * 1.5 + HEX_UNIT_SIDE_LENGTH));
}
}

return spaces;

/**
* @param {number} primary
* @param {number} secondary
*/
function coordinate(primary, secondary) {
return isColumnar ? { x: secondary, y: primary } : { x: primary, y: secondary };
}
}
);

/**
* @param {number} x Token X.
* @param {number} y Token Y.
* @param {number} width Token width (in grid spaces).
* @param {number} height Token height (in grid spaces).
* @param {number} gridType The type of grid.
* @param {number} gridSize The size of the grid in pixels.
* @param {number} hexShape For hexagonal tokens, the type of hex shape used.
*/
export function getSpacesUnderToken(x, y, width, height, gridType, gridSize, hexShape) {
// Gridless is not supported
if (gridType === CONST.GRID_TYPES.GRIDLESS) {
return [];
}

// For square, can easily work the points out by enumerating over the width/height
if (gridType === CONST.GRID_TYPES.SQUARE) {
return getSquareTokenSpaces(width, height)
.map(p => ({ x: x + p.x * gridSize, y: y + p.y * gridSize }));
}

// For hex grids, it depends on the token's hex shape:
// Hex grids are also rotationally equivalent (i.e. for a hex row we can just swap X and Y from a hex column).
// We define a "primary" axis (the direction in the namesake of the grid - i.e. Y/height for columns and X/width for
// rows). The "secondary" axis is the other (X/height for columns, Y/height for rows).
const isColumnar = [CONST.GRID_TYPES.HEXEVENQ, CONST.GRID_TYPES.HEXODDQ].includes(gridType);
const primaryAxisSize = isColumnar ? height : width;
const secondaryAxisSize = isColumnar ? width : height;
const isVariant2 = [
CONST.TOKEN_HEXAGONAL_SHAPES.ELLIPSE_2,
CONST.TOKEN_HEXAGONAL_SHAPES.TRAPEZOID_2,
CONST.TOKEN_HEXAGONAL_SHAPES.RECTANGLE_2
].includes(hexShape);

switch (hexShape) {
case CONST.TOKEN_HEXAGONAL_SHAPES.ELLIPSE_1:
case CONST.TOKEN_HEXAGONAL_SHAPES.ELLIPSE_2:
return getEllipseHexTokenSpaces(primaryAxisSize, secondaryAxisSize, isColumnar, isVariant2)
.map(p => ({ x: x + p.x * gridSize, y: y + p.y * gridSize }));

case CONST.TOKEN_HEXAGONAL_SHAPES.TRAPEZOID_1:
case CONST.TOKEN_HEXAGONAL_SHAPES.TRAPEZOID_2:
return getTrapezoidHexTokenSpaces(primaryAxisSize, secondaryAxisSize, isColumnar, isVariant2)
.map(p => ({ x: x + p.x * gridSize, y: y + p.y * gridSize }));

case CONST.TOKEN_HEXAGONAL_SHAPES.RECTANGLE_1:
case CONST.TOKEN_HEXAGONAL_SHAPES.RECTANGLE_2:
return getRectangleHexTokenSpaces(primaryAxisSize, secondaryAxisSize, isColumnar, isVariant2)
.map(p => ({ x: x + p.x * gridSize, y: y + p.y * gridSize }));

default:
throw new Error("Unknown hex grid type.");
}
}
21 changes: 21 additions & 0 deletions module/utils/misc-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,24 @@ export class OrderedSet {
};
}
}

/**
* Wraps a function such that the return value of the function is cached based on the parameters passed to it.
* @template {(...args: any) => any} T
* @param {T} func Function to wrap.
* @param {(args: Parameters<T>) => string} keyFunc Optional function to generate cache key from the arguments. If not
* provided, defaults to all arguments joined by a "|".
* @returns {T}
*/
export function cacheReturn(func, keyFunc = undefined) {
const cache = new Map();
keyFunc ??= args => args.join("|");

return function(...args) {
const key = keyFunc(args);
if (cache.has(key)) return cache.get(key);
const result = func(...args);
cache.set(key, result);
return result;
}
}

0 comments on commit 9b01638

Please sign in to comment.