The pack
nav
function nav(): Pack<{ nav: NavApi }>
The nav pack: A* over a walkable grid with string pulling, and a flow field for when many things
head to one place, in 3D (world X/Z over a height function) or 2D (world X/Y over a tile map), for a point
or a body of a given radius. Headless and pure — it holds no objects and draws nothing, so it runs in a
worker or a test as happily as in a frame.
Classes
FlowField
class FlowField
A per-cell direction field toward a goal (Tier 2).
constructor( /** The NavGrid the field was integrated over. */ readonly grid: NavGrid, /** The integrated cost to the goal for each cell. Infinity where unreachable. */ readonly cost: Float32Array, /** The X part of the unit steering direction for each cell. */ readonly dirx: Float32Array, /** The Z part of the unit steering direction for each cell. */ readonly dirz: Float32Array, )dirAt(x: number, z: number): { x: number; z: number }— Steering direction at a world XZ (zero where unreachable/at goal).reachable(x: number, z: number): boolean— Whether a world XZ can reach the goal at all (finite cost).
FlowField2d
class FlowField2d
A per-cell direction field toward a goal over a NavGrid2d (Tier 2, in 2D).
constructor( /** The NavGrid2d the field was integrated over. */ readonly grid: NavGrid2d, /** The integrated cost to the goal for each cell. Infinity where unreachable. */ readonly cost: Float32Array, /** The X part of the unit steering direction for each cell. */ readonly dirx: Float32Array, /** The Y part of the unit steering direction for each cell. */ readonly diry: Float32Array, )dirAt(x: number, y: number): NavVec2— Steering direction at a world XY (zero where unreachable/at goal).reachable(x: number, y: number): boolean— Whether a world XY can reach the goal at all (finite cost).
FlowGrid
class FlowGrid
A mutable grid carrying a flow field toward one or more goals. Build it, set
goals + obstacles, then read steer() / flowVec() / distance() every
frame. Edits (block, weight, goals) mark the field dirty; it rebuilds itself
on the next query — call update up front if you want the cost paid at
a known time.
const grid = new FlowGrid({ cols: 40, rows: 30, goals: [{ col: 39, row: 15 }], spawns: [{ col: 0, row: 15 }] });
if (grid.canBuild(c, r)) grid.build(c, r); // place a tower
enemy.vel = scale(grid.steer(enemy.x, enemy.y, TILE), speed); // follow the flow
readonly cols: number— Grid width in cells.readonly rows: number— Grid height in cells.diagonal: boolean— Whether diagonal movement is allowed.cutCorners: boolean— Whether diagonal moves may cut blocked corners.goals: Cell[]— The current goal cells (flow points toward the nearest).spawns: Cell[]— The current spawn cells (used bycanBuild).readonly blocked: Uint8Array—1where a cell is blocked (tower/wall), else0. Row-major.readonly weight: Float32Array— Per-cell entry cost (terrain weight);1is normal, higher is slower.readonly dist: Float32Array— Integration field: distance to the nearest goal per cell,Infinitywhen unreachable. Row-major. Rebuilt byupdate.@internal-ish — preferdistance.readonly flow: Int8Array— Flow field: theFlowDireach cell steps toward its goal.constructor(opts: FlowGridOptions)index(col: number, row: number): number— Row-major cell index for(col, row). No bounds check.inBounds(col: number, row: number): boolean— Is(col, row)inside the grid?cellAt(x: number, y: number, cellSize = 1, originX = 0, originY = 0): Cell | null— Which cell contains world/pixel position(x, y), atcellSizeunits per cell from(originX, originY).nullwhen outside the grid.isBlocked(col: number, row: number): boolean— Is the cell an obstacle (tower/wall)? Out-of-bounds reads as blocked.setBlocked(col: number, row: number, on: boolean): void— Block or unblock a cell (place/remove a tower or wall).build(col: number, row: number): void— Place a tower/wall at(col, row)(alias forsetBlocked(…, true)).unbuild(col: number, row: number): void— Remove a tower/wall at(col, row)(alias forsetBlocked(…, false)).clearBlocked(): void— Clear every obstacle.setWeight(col: number, row: number, w: number): void— Set a cell's terrain entry cost (1normal;>1slow mud;<1fast road). Cells stay passable; block withsetBlockedinstead.setGoals(goals: Cell[]): void— Replace the goal cells and mark the field dirty.setSpawns(spawns: Cell[]): void— Replace the spawn cells (used only bycanBuild).update(): void— Rebuild the integration + flow fields (Dijkstra from all goals). Called automatically by the queries when the grid is dirty; call it yourself to pay the cost at a controlled time. Orthogonal steps costweight, diagonal stepsweight · √2.distance(col: number, row: number): number— Distance (field cost) from(col, row)to the nearest goal, orInfinityif blocked/unreachable.reachable(col: number, row: number): boolean— Can an enemy at(col, row)reach a goal?flowDir(col: number, row: number): FlowDir— TheFlowDir(col, row)flows toward its goal (-1at a goal / blocked / unreachable cell).flowVec(col: number, row: number): NavVec2— The unit flow VECTOR at cell(col, row)— the direction to move, as a{x, y}(zero when there's no flow).steer(x: number, y: number, cellSize = 1, originX = 0, originY = 0): NavVec2— The smooth steering direction for an enemy at world position(x, y)— bilinearly interpolates the flow arrows of the four surrounding cells so units off the cell centre don't jerk between headings. Returns a unit-length{x, y}(zero at the goal / in a dead pocket).cellSize+originmap world units to the grid (same convention ascellAt).canBuild(col: number, row: number): boolean— Would blocking(col, row)still leave EVERY spawn able to reach a goal? The tower-placement guard: returnsfalsefor a build that would fully wall a spawn off (or that isn't a legal, currently-open cell). Runs a connectivity check with the same movement rules — it does not mutate the grid.path(from: Cell, maxSteps = this.cols * this.rows): Cell[]— Trace the route from(col, row)by following the flow field to a goal — an inclusive list of cells, for drawing a preview path or a debug overlay. Empty when unreachable; capped so a broken field can't loop forever.
NavGrid
class NavGrid extends NavGridCore
A walkability grid over a heightfield, for A* paths and flow fields.
readonly res: number— The number of cells per side of the square grid.readonly ox: number— The world X of the center of cell (0,0).readonly oz: number— The world Z of the center of cell (0,0).readonly height: Float32Array— The sampled surface height of each cell, indexed by idx().constructor(o: NavGridOptions)snapshot(): NavGridSnapshot— A structured-cloneable copy of the grid, for a worker.static fromSnapshot(s: NavGridSnapshot): NavGrid— Rebuilds a grid from snapshot().idx(ix: number, iz: number): number— Returns the flat array index of the cell (ix, iz).inBounds(ix: number, iz: number): boolean— Returns true when the cell (ix, iz) is inside the grid.cellCenter(ix: number, iz: number): NavVec3— Returns the world center of the cell, with y set to the cell's surface height.worldToCell(x: number, z: number): { ix: number; iz: number }— Returns the cell nearest to a world XZ, clamped to the grid.walkableCell(ix: number, iz: number): boolean— Returns true when the cell is inside the grid and walkable.waypoint(i: number, radius?: number): NavVec3 | null— The world point a route for a body ofradius(default the grid's) passes through in celli: its center when the body fits there, else the cell's roomiest edge midpoint or corner the body fits on, at the cell's surface height; null when it fits nowhere in the cell.clear(from: NavVec3, to: NavVec3, options?: NavQueryOptions): boolean— Can a body ofoptions.radius(default the grid's) slide straight from one world point to another over X/Z, without touching an unwalkable cell, crossing an impassable step or leaving the grid? Exact. A follower asks it to turn for the next waypoint as soon as it can reach it directly.nearestWalkable(ix: number, iz: number, radius?: number): { ix: number; iz: number } | null— The nearest cell to (ix,iz) a body ofradius(default the grid's) fits in (start/goal snap). A point takes the nearest walkable cell by ring search; a body, the nearest cell it fits in that it can walk to from there.astar(startX: number, startZ: number, goalX: number, goalZ: number, radius?: number): number[] | null— A* over 8-connected cells between two world XZ points, for a body ofradius(default the grid's). Returns a list of cell indices start→goal, or null if unreachable (a cliff-ringed peak refuses a path).path(start: NavVec3, goal: NavVec3, options?: NavQueryOptions): NavVec3[]— Full plan start→goal as WORLD waypoints at surface height, for a body ofoptions.radius(default the grid's): cell centers, or for a body too wide for a cell's center, the point of that cell it fits on. Empty array when no path exists.flowField(goal: NavVec3, options?: NavQueryOptions): FlowField— Build a flow field: cost-to-goal by Dijkstra, then a per-cell direction toward the cheapest neighbor, for bodies ofoptions.radius(default the grid's). Every agent samples it O(1).
NavGrid2d
class NavGrid2d extends NavGridCore
A walkability grid over a tile map in world X/Y (2D, y down), for A* paths and flow fields.
readonly ox: number— The world X of the center of cell (0,0).readonly oy: number— The world Y of the center of cell (0,0).constructor(o: NavGrid2dOptions)snapshot(): NavGrid2dSnapshot— A structured-cloneable copy of the grid, for a worker.static fromSnapshot(s: NavGrid2dSnapshot): NavGrid2d— Rebuilds a grid from snapshot().idx(col: number, row: number): number— Returns the flat array index of the cell (col, row).inBounds(col: number, row: number): boolean— Returns true when the cell (col, row) is inside the grid.cellCenter(col: number, row: number): NavVec2— Returns the world center of the cell.worldToCell(x: number, y: number): { col: number; row: number }— Returns the cell under a world XY, clamped to the grid.walkableCell(col: number, row: number): boolean— Returns true when the cell is inside the grid and walkable.waypoint(i: number, radius?: number): NavVec2 | null— The world point a route for a body ofradius(default the grid's) passes through in celli: its center when the body fits there, else the cell's roomiest edge midpoint or corner the body fits on; null when it fits nowhere in the cell.clear(from: NavVec2, to: NavVec2, options?: NavQueryOptions): boolean— Can a body ofoptions.radius(default the grid's) slide straight from one world point to another without touching an unwalkable cell or leaving the grid? Exact. A follower asks it to turn for the next waypoint as soon as it can reach it directly.nearestWalkable(col: number, row: number, radius?: number): { col: number; row: number } | null— The nearest cell to (col, row) a body ofradius(default the grid's) fits in. A point takes the nearest walkable cell by ring search; a body, the nearest cell it fits in that it can walk to from there.astar(startX: number, startY: number, goalX: number, goalY: number, radius?: number): number[] | null— A* over 8-connected cells between two world XY points, for a body ofradius(default the grid's). Returns a list of cell indices start→goal, or null if unreachable.path(start: NavVec2, goal: NavVec2, options?: NavQueryOptions): NavVec2[]— Full plan start→goal as world XY waypoints, string-pulled for a body ofoptions.radius(default the grid's): cell centers, or for a body too wide for a cell's center, the point of that cell it fits on. Empty array when no path exists.flowField(goal: NavVec2, options?: NavQueryOptions): FlowField2d— Build a flow field toward one goal for bodies ofoptions.radius(default the grid's). Every agent samples it O(1).
NavGridCore
abstract class NavGridCore
The cell graph NavGrid and NavGrid2d share: walkability, the clearance map, A*, line of sight, string
pulling and flow-field integration, over cols × rows cells indexed row * cols + col. Build a NavGrid or
a NavGrid2d; this is their common part, not a grid on its own.
A route runs through one point per cell: the cell's center, or, for a body too wide to stand there, the roomiest of the cell's edge midpoints and corners it fits on. So a body fits any gap between walls that is at least twice its radius wide, as walls are whole cells and the middle of a gap between them is always a center, an edge or a corner.
readonly cols: number— The number of cells across.readonly rows: number— The number of cells down (along Z on a NavGrid, Y on a NavGrid2d).readonly cell: number— The size of one square cell, in world units.readonly walk: Uint8Array— Walkability of each cell, row by row: 1 walkable, 0 blocked. Change it with setWalkable(), or call invalidate() after editing it in place.readonly radius: number— The default walker radius, in world units, for queries that do not give their own. 0 is a point.get clearance(): Float32Array— The distance from each cell's center to the nearest unwalkable cell or the grid's edge, in world units, row by row (0 on an unwalkable cell). A body of radius r can stand at a cell's center when its clearance is at least r. Worked out on first use, and again after a change towalk.invalidate(): void— Forget the cached clearance, afterwalkhas been edited in place. setWalkable() does this itself.setWalkable(i: number, walkable: boolean): void— Make celliwalkable or not, and forget the cached clearance. Throws for an index off the grid.fits(i: number, radius?: number): boolean— Returns true when a body ofradius(default the grid's) can stand somewhere in celli: at its center, an edge midpoint or a corner, that far from every unwalkable cell and the grid's edge.lineOfSight(a: number, b: number, radius?: number): boolean— Walkable line of sight between two cells, by index, for a body ofradius(default the grid's): true only when a body that size can slide straight from where it stands in one to where it stands in the other without touching an unwalkable cell or leaving the grid. Exact, not sampled.stringPull(cells: number[], radius?: number): number[]— String-pull: drop interior cells the previous kept cell can see directly — for a body ofradius(default the grid's), so a route is straightened only where the whole body fits.
Types
Cell
interface Cell
A grid cell by column and row.
col: number— Column, 0-based.row: number— Row, 0-based.
FlowDir
type FlowDir = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
A flow direction as an 8-way index (0=E, 1=SE, 2=S, 3=SW, 4=W, 5=NW, 6=N,
7=NE), or -1 for "no flow" (a goal cell, a blocked cell, or unreachable).
FlowGridOptions
interface FlowGridOptions
Configuration for a new FlowGrid. cols/rows are required.
cols: number— Grid width in cells.rows: number— Grid height in cells.goals?: Cell[]— Goal cell(s) — the field flows toward the NEAREST of these (multi-goal is the "supernode" trick, seeded for free). At least one is needed for a useful field.spawns?: Cell[]— Spawn cell(s). Only used byFlowGrid.canBuildto guarantee a tower never fully walls a spawn off from the goal.diagonal?: boolean— Allow 8-way (diagonal) movement. Defaulttrue.cutCorners?: boolean— Let diagonal moves slip between two blocked cells at a corner. Defaultfalse(units won't squeeze between two towers placed corner-to-corner).
NavApi
interface NavApi
game.nav.
grid(opts: NavGridOptions): NavGrid— Build a walkable grid over a height function in world X/Z — the A* tier for a 3D game.grid2d(opts: NavGrid2dOptions): NavGrid2d— Build a walkable grid over a tile map in world X/Y (y down) — the A* tier for a 2D game.flow(grid: NavGrid, goal: NavVec3, options?: NavQueryOptions): FlowField— A flow field toward one goal over a grid — the many-agents-one-target tier: O(1) per lookup. A flow field toward one goal over a 2D grid — the many-agents-one-target tier: O(1) per lookup.flow(grid: NavGrid2d, goal: NavVec2, options?: NavQueryOptions): FlowField2d— A flow field toward one goal over a grid — the many-agents-one-target tier: O(1) per lookup. A flow field toward one goal over a 2D grid — the many-agents-one-target tier: O(1) per lookup.pathAsync(grid: NavGrid, start: NavVec3, goal: NavVec3, options?: NavAsyncOptions): Promise<NavVec3[]>— Run A* off-thread on a copied snapshot. Requires a started game and bakery worker output. Run 2D A* off-thread on a copied snapshot. Requires a started game and bakery worker output.pathAsync(grid: NavGrid2d, start: NavVec2, goal: NavVec2, options?: NavAsyncOptions): Promise<NavVec2[]>— Run A* off-thread on a copied snapshot. Requires a started game and bakery worker output. Run 2D A* off-thread on a copied snapshot. Requires a started game and bakery worker output.flowGrid(opts: FlowGridOptions): FlowGrid— A weighted flow grid for tower defence and crowds on a flat grid: per-cell weights, several goals,canBuild("would this wall cut the last route?") and smoothsteer. Its walkers are points.readonly last: NavGrid | null— The last grid built bygrid, for a game that only needs one.readonly last2d: NavGrid2d | null— The last grid built bygrid2d, for a game that only needs one.
NavAsyncOptions
interface NavAsyncOptions
Options for game.nav.pathAsync.
signal?: AbortSignal— Cancels the job.radius?: number— The walker's radius for this path, in world units, in place of the grid'sradius.partial?: boolean— When the goal cannot be reached, a route to the reachable cell nearest it rather than none. Default false.
NavGrid2dOptions
interface NavGrid2dOptions
Options for the NavGrid2d constructor: a tile map over world X/Y, y down.
cols: number— Cells across: a whole number, at least 1.rows: number— Cells down: a whole number, at least 1.cell?: number— The size of one square cell, in world units (default 1). With a tile map this is usually the tile size.origin?: { x: number; y: number }— The world position of the grid's top-left corner (default 0,0). Cell (col, row) covers x fromorigin.x + col * celland y fromorigin.y + row * cell, one cell each way.walkable?: ArrayLike<number | boolean>— Walkability per cell, row by row (row * cols + col), exactlycols * rowslong: truthy is walkable. Omit for an open floor.blocked?: (x: number, y: number) => boolean— Extra per-cell mask, asked at each cell's center in world X/Y: return true to block. Combines withwalkable.radius?: number— The walker's radius, in world units (default 0, a point). Works as NavGridOptions.radius does.
NavGrid2dSnapshot
interface NavGrid2dSnapshot
Structured-cloneable NavGrid2d. The buffer is a copy, safe to transfer.
kind: '2d'— Marks a 2D grid snapshot.cols: number— Cells across.rows: number— Cells down.cell: number— The size of one cell, in world units.origin: { x: number; y: number }— The world position of the grid's top-left corner.radius: number— The grid's default walker radius.walk: Uint8Array— Walkability per cell, row by row: 1 walkable, 0 blocked.
NavGridOptions
interface NavGridOptions
Options for the NavGrid constructor.
heightAt: (x: number, z: number) => number— Surface height at a world XZ (usuallyterrain.heightAt).size: number— World span of the (square) grid.center?: { x: number; z: number }— Grid center in world XZ (default origin).res?: number— Cells per side (default 64). Coarser than the render mesh is fine.maxStep?: number— Steeper than this height-step BETWEEN adjacent cells is a cliff — cut.slopePenalty?: number— Cost multiplier for climbing (0 = ignore slope, 3 = strongly prefer flat).minY?: number— Cells whose surface is below this Y are unwalkable (water/void).blocked?: (x: number, z: number) => boolean— Extra per-cell mask, asked at each cell's center in world X/Z: return true to block (obstacles, no-go zones).radius?: number— The walker's radius, in world units (default 0, a point). Every path and flow field on this grid plans for a body this size unless the call gives its own: A* refuses gaps narrower than the body, and string pulling straightens a route only where the whole body fits along the straight line.
NavGridSnapshot
interface NavGridSnapshot
Structured-cloneable sampled grid. Buffers are copies, safe to transfer.
res: number— Cells per side of the square grid.size: number— World span of the grid along each side, in world units.center: { x: number; z: number }— The grid's centre in world X and Z, in world units.maxStep: number— The tallest height step between adjacent cells that still counts as walkable, in world units.slopePenalty: number— Cost multiplier for climbing (0 ignores slope; larger prefers flat ground).height: Float32Array— Sampled surface height of each cell, in world units, row by row (iz * res + ix):res * reslong.walk: Uint8Array— Walkability of each cell, row by row (iz * res + ix): 1 walkable, 0 blocked.res * reslong.radius?: number— The grid's default walker radius, in world units; a snapshot without one reads as 0.
NavPathJob
type NavPathJob = | { grid: NavGridSnapshot; start: NavVec3; goal: NavVec3; radius?: number; partial?: boolean } | { grid: NavGrid2dSnapshot; start: NavVec2; goal: NavVec2; radius?: number; partial?: boolean }
One off-thread path job: a grid snapshot (3D or 2D), the two ends, an optional walker radius in place of the grid's, and whether an unreachable goal gets a partial route.
NavQueryOptions
interface NavQueryOptions
Per-query options for a path or a flow field.
radius?: number— The walker's radius for this query, in world units, in place of the grid'sradius. 0 plans for a point.partial?: boolean— A path only: when the goal cannot be reached, a route to the reachable cell nearest it (as the crow flies) rather than none, so a chaser walls in as close as it can get. Default false.nav-meshtakes the same option.
NavVec2
interface NavVec2
A 2D world position (x and y), in world units. y points down the screen.
x: number— The X position, in world units.y: number— The Y position, in world units, down the screen.
NavVec3
interface NavVec3
A world position (x, y and z), in world units. y is the surface height.
x: number— The X position, in world units.y: number— The surface height, in world units.z: number— The Z position, in world units.
PathPlanner
interface PathPlanner
What a planner must offer, so a behavior can steer over any of them. A NavGrid is one.
path(start: NavVec3, goal: NavVec3, options?: NavQueryOptions): NavVec3[]— A route from start to goal, world points, or an empty list when there is none.options.radiusasks for a route that a body of that radius fits along; a planner that cannot vary it (a baked navmesh) ignores it.clear?(from: NavVec3, to: NavVec3, options?: NavQueryOptions): boolean— Optional: can a body ofoptions.radiusslide straight from one point to another? A follower that has it turns for the next waypoint as soon as it can reach it directly. A NavGrid has it.
PathPlanner2d
interface PathPlanner2d
What a 2D planner must offer: PathPlanner in world X/Y. A NavGrid2d is one.
path(start: NavVec2, goal: NavVec2, options?: NavQueryOptions): NavVec2[]— A route from start to goal, world X/Y points, or an empty list when there is none, for a body ofoptions.radiuswhen the planner can vary it.clear?(from: NavVec2, to: NavVec2, options?: NavQueryOptions): boolean— Optional: can a body ofoptions.radiusslide straight from one point to another? A NavGrid2d has it.


