Skip to content

API - Spatial & Distance Tools

Back to API Reference · Feature guide: Vision


Distance Calculations

Three distance functions. All return distance in grid spaces (not pixels).

Function Input Size-aware Use case
getTokenDistance Two tokens Yes General token-to-token distance. Wraps getMinGridDistance.
getMinGridDistance Two tokens + optional override pos + optional elevation flag Yes Iterates all occupied cells of both tokens, returns the shortest cell-to-cell distance. Supports hypothetical positioning via overridePos1. With includeElevation, distance is max(horizontal, elevation).
getGridDistance Two {x,y} world points No Raw point-to-point grid distance. Use when you have coordinates, not tokens.

To find the tokens themselves rather than measure a known pair, use getTokensInRange.

getTokenDistance → number


api.getTokenDistance(token1, token2, includeElevation)
Param Type Description
token1 Token First token
token2 Token Second token
includeElevation boolean true returns max(horizontal, elevation). Omit it to follow the count3DDistance setting
const dist = api.getTokenDistance(reactorToken, moverToken);
if (dist > 3) return false;
getMinGridDistance → number


api.getMinGridDistance(token1, token2, overridePos1, includeElevation)

With includeElevation, the result is max(planar distance, elevation difference) in grid spaces: the dominant axis wins, so 1 horizontal + 2 vertical = 2.

Param Type Default Description
token1 Token required First token
token2 Token required Second token
overridePos1 { x: number; y: number } null Evaluate as if token1 were at this world position
includeElevation boolean count3DDistance setting If true, the planar distance competes with \|elevation1 − elevation2\| (in grid spaces) and the larger wins
const planar = api.getMinGridDistance(tokenA, tokenB);
const withElevation = api.getMinGridDistance(tokenA, tokenB, null, true);
getGridDistance → number


api.getGridDistance(pos1, pos2)

Hex grids: cube distance. Square grids: measurePath rounded to grid units.

Param Type Description
pos1 { x: number; y: number } World coordinates
pos2 { x: number; y: number } World coordinates
const spaces = api.getGridDistance(token.center, { x: 1200, y: 800 });
getTokensInRange → Token[]


api.getTokensInRange(origin, options)

Tokens within range spaces of a token or a world point, nearest first. Size-aware on both ends. range: 1 is adjacency.

Param Type Default Description
origin Token or { x, y, elevation? } required Measured from every cell of the token, or from the point's cell
range number or 'sensors' 1 Spaces. 'sensors' reads the origin actor's sensor range
disposition 'friendly' | 'hostile' any Faction-correct, Token Factions aware
includeSelf boolean false
includeHidden boolean false
includeDefeated boolean false Structure or stress at 0
includeDeployables boolean true
engageable boolean false Also apply canEngage: hostile, non-deployable, structure above 0, no hidden/disengage/intangible, no provoke immunity
includeElevation boolean count3DDistance setting A point origin is always elevation-aware
filter (token) => boolean null

A point origin ignores disposition, engageable and includeSelf.

const adjacent = api.getTokensInRange(reactorToken);
const engaged = api.getTokensInRange(reactorToken, { engageable: true });
const allies = api.getTokensInRange(reactorToken, { range: 3, disposition: 'friendly' });
const nearBlast = api.getTokensInRange(template.center, { range: 2 });
getEngagedTokens → Token[]


api.getEngagedTokens(token, options)

The tokens token is engaged with. Empty unless token itself carries the engaged status, and the returned tokens carry it too. The status is read from both the flagged effect and the actor status, so GM-applied ones count.

Param Type Default Description
token Token required The engaged token to measure from
includeElevation boolean count3DDistance setting
filter (token) => boolean null

Range and engageable are fixed: engagement is adjacency plus canEngage, so deployables, dead mechs, and anything hidden / disengage / intangible never appear.

const engaged = api.getEngagedTokens(targetToken);
const others = api.getEngagedTokens(targetToken, { filter: t => t.id !== attackerToken.id });
getTokenPosition → { x, y, elevation }
samePosition → boolean


api.getTokenPosition(tokenLike)   // → { x, y, elevation }
api.samePosition(a, b)            // same x / y / elevation

Snapshot a token's position and compare it later ("has it moved since?"). tokenLike is a Token or TokenDocument, a/b are position objects.

getTokenPosition returns the token's top-left corner (doc.x / doc.y), not its center. It is not a drop-in destination for moveToken, which expects a center point.


Grid Coordinate Helpers

Square + hex. "Center" points drop straight into moveToken({ destination }).

Function Returns Purpose
getCellToward(from, toward, { steps=1, away=false }) { x, y } center Cell steps from from toward (or away from) toward, walking real neighbors. from/toward = Token or point.
snapTokenCenter(token, center) { x, y } top-left Snap a center to a valid placement for the token footprint.
getOccupiedCenters(token, overridePos?) Array<{ x, y }> Centers of every cell the token occupies.
getHexCenter(col, row) { x, y } center Cell center from a grid offset.
pixelToOffset(x, y) { col, row } Grid offset at a world point.
measureGridDistance(p1, p2) number Distance between two points in scene units, not grid spaces. Divide by canvas.scene.grid.distance for spaces, or use getGridDistance.
neighborKeys("col,row") string[] Adjacent cell keys (6 hex / 8 square).

Line of Sight

Beta. Wall-based, height-aware line of sight - the same test the Lancer LOS detection mode runs. Only meaningful with Lancer Line of Sight enabled in the Vision tab.

hasLineOfSight → boolean


api.hasLineOfSight(refA, refB)

True if refA has a clear Lancer line of sight to refB. Each argument is a Token, TokenDocument, or token id. Returns false if either can't be resolved.

The wall test is reciprocal: if A sees B, B sees A. Blinded is not. With the blindedSetsVision setting on, a Blinded token sees adjacent spaces only, and that cuts sight for it alone, so the pair can disagree. Fails open: a token paired with itself, or a destroyed placeable, returns true.

Param Type Description
refA Token \| TokenDocument \| string Token, document, or id
refB Token \| TokenDocument \| string Token, document, or id
if (!api.hasLineOfSight(reactorToken, targetToken)) return false;

Faction & Disposition

isHostile → boolean


api.isHostile(reactor, mover)

True when one side is friendly and the other hostile. NEUTRAL counts as friendly, SECRET counts as hostile. Two hostile tokens are not hostile to each other. Compatible with the Token Factions module, which answers the question itself when active.

Param Type Description
reactor Token The reacting token
mover Token The triggering token
if (!api.isHostile(reactorToken, moverToken)) return false;
canProvokeReaction → boolean


api.canProvokeReaction(triggering, reactor, reasonOut?)

false when the triggering token cannot provoke: it is hidden, took disengage, carries the provoke immunity, or is intangible while the reactor is not. A token paired with itself always provokes. This is the same gate the engine applies before offering a reaction, exposed for your own filters.

Param Type Default Description
triggering Token required The token that would provoke
reactor Token required The token that would react
reasonOut Array<string> null Pass an array. The blocking reason is pushed onto it: hidden, disengage, provoke_immunity or intangible
const reasons = [];
if (!api.canProvokeReaction(moverToken, reactorToken, reasons) && reasons.includes('disengage'))
    console.log('mover disengaged');
isFriendly → boolean


api.isFriendly(token1, token2)

True when both sides sit on the same side of the line: both friendly, or both hostile. NEUTRAL counts as friendly, SECRET counts as hostile. Two mutually hostile tokens are therefore friendly to each other, so this is not the inverse of isHostile. Compatible with the Token Factions module, which answers the question itself when active.

Param Type Description
token1 Token First token
token2 Token Second token
const allies = canvas.tokens.placeables.filter(t => api.isFriendly(casterToken, t));
getRelativeDisposition → number|null


api.getRelativeDisposition(viewer, other)

Disposition of other as seen from viewer, returned as a CONST.TOKEN_DISPOSITIONS value. It resolves the advanced-team matrix only when Token Factions is active and its "color from" setting is advanced-factions, the one mode that carries the full matrix. Otherwise it falls back to other's own token disposition. Use instead of token.disposition for faction-correct results.

Param Type Description
viewer Token The reference token (perspective)
other Token The token being classified
const hostile = api.getRelativeDisposition(viewerToken, otherToken) === CONST.TOKEN_DISPOSITIONS.HOSTILE;

Grid & Cell Data

getTokenCells → Array<[row, col]>


api.getTokenCells(token)
Param Type Description
token Token The token to inspect
const occupied = new Set(api.getTokenCells(token).map(([row, col]) => `${col},${row}`));
laTokenHeight → number
laTokenGameplayHeight → number


api.laTokenHeight(tokenDoc)
api.laTokenGameplayHeight(tokenDoc)
Param Type Description
tokenDoc TokenDocument The token to measure

laTokenHeight: sight height, wall-height flag or SIZE + 0.1. laTokenGameplayHeight: the same snapped to the closest SIZE (0.5, 1, 2, 3...).

getMaxGroundHeightUnderToken → number


api.getMaxGroundHeightUnderToken(token, terrainAPI)

Highest terrain top (elevation + height) under any cell the token occupies, in scene units. Only terrain types that are both solid and height-using count, so decorative or non-solid terrain is skipped. Returns 0 with no terrain or no Terrain Height Tools.

Param Type Description
token Token The token to check
terrainAPI Object Optional. Terrain Height Tools API object, defaults to globalThis.terrainHeightTools
const tht = game.modules.get("terrain-height-tools")?.api;
const ground = api.getMaxGroundHeightUnderToken(token, tht);
triggerDangerousZoneFlow async → void


await api.triggerDangerousZoneFlow(token, damageType, damageValue)

Rolls an ENG check on the token's actor. On a result below 10 the token is targeted and a damage roll is performed. Dedupes to once per combat round per actor (uses an actor flag in the lancer-automations namespace). Outside combat, fires every call.

If the actor is terrain-immune (the terrain_immunity status or a terrain immunity bonus), a "TERRAIN IMMUNITY" choice card goes to the GM first: ignore the terrain, which consumes an immunity use when the immunity came from a bonus, or apply it anyway and run the check.

Body for a "dangerous terrain" trigger, e.g. a Terrain Height Tools on-enter callback:

await game.modules.get("lancer-automations").api.triggerDangerousZoneFlow(token, "burn", 5);
Param Type Description
token Token \| TokenDocument Token whose actor rolls ENG and takes damage on failure
damageType string "kinetic", "energy", "explosive", "burn", "heat", "variable". Defaults to "kinetic"
damageValue number \| string Damage amount or dice expression. Defaults to 5

Designed for Pilot/Mech actors. NPCs do not have a direct system.eng and the flow returns silently.


Debug Visualizations

drawThreatDebug async
drawDistanceDebug async → void


await api.drawThreatDebug(token)    // Draws threat range cells on canvas. Hex grids only.
await api.drawDistanceDebug()       // Select 2 tokens, draws shortest distance line.

Params: token Token

await api.drawThreatDebug(canvas.tokens.controlled[0]);
drawRangeHighlight → PIXI.Graphics


api.drawRangeHighlight(casterToken, range, color, alpha, includeSelf, opts)
Param Type Default Description
casterToken Token\|{ x: number; y: number } required Origin token or point
range number required Radius in grid spaces
color number 0x00ff00 Hex color
alpha number 0.2 Opacity (0-1)
includeSelf boolean false Include origin cells
opts Object {} Styling: lineAlpha, lineColor, lineWidth, glowColor, perimeterAlpha. Also los, which clips the highlight to line of sight and needs the rangePulseLos setting, and freeRange, spaces around the origin the clip never removes
const gfx = api.drawRangeHighlight(casterToken, 5, 0xff6400, 0.15);
gfx.destroy();