Install
Run npm pack ./dist/packages/wayfinder from the repository and install the resulting archive in your game project:
npm install ./cranberry-forge-wayfinder-0.1.0.tgzThe archive is an installable npm package. This documentation does not assume publication to the public npm registry.
import { Wayfinder } from '@cranberry-forge/wayfinder';
const garden = new Wayfinder({
width: 12,
height: 10,
cellSize: 1.35,
origin: { x: -8.1, z: -6.75 },
diagonal: 'no-cut',
});
garden.setCells([
{ x: 3, z: 2, blocked: true },
{ x: 4, z: 3, cost: 5 },
]);
const route = garden.findPath({ x: 1, z: 8 }, { x: 10, z: 1 }, { y: 1.05 });
if (route.status === 'found') {
console.log(route.cost, route.visited);
console.log(route.cells); // [{ x, z }, ...], including start and goal
console.log(route.points); // [{ x, y, z }, ...], at cell centers
}The navigation contract
- One rectangular, single-layer XZ grid. Coordinates are zero-based
{ x, z }integer indices.originis the lower X/Z boundary, not the first cell center. World bounds include the lower edge and exclude the upper edge. - Entering a neighboring cell costs its weight ×
cellSize× step length. Cardinal step length is 1; diagonal step length is √2. The starting cell's weight is not charged. Weights must be finite numbers in 1–1,000. diagonal: 'never'allows four neighbors.'no-cut', the default, allows diagonals only when both adjacent cardinal cells are open.'always'allows diagonal movement past blocked corners. All policies still forbid entering blocked cells.- A* uses Manhattan or octile distance with the minimum allowed weight of 1. Its heuristic remains admissible as terrain weights change. An indexed binary heap holds at most one open entry per cell.
- Ties compare total estimate
f, then heuristich, then row-major cell index. Equal-cost alternate parents do not replace the first parent. Repeating a search with the same grid and endpoints gives the same result in the same JavaScript numeric environment. - A successful search returns an optimal weighted route on this graph, subject to ordinary floating-point arithmetic. An unsuccessful search returns an explicit status and no partial route. A bounded search may return
budget-exceededeven when a route exists.
Edit terrain and replan
const previous = garden.findPath({ x: 1, z: 8 }, { x: 10, z: 1 });
garden.setCell({ x: 5, z: 4 }, { blocked: true });
if (previous.revision !== garden.revision) {
// The old route describes an earlier terrain revision. Search again.
const current = garden.findPath({ x: 1, z: 8 }, { x: 10, z: 1 });
}setCells() validates the whole batch before changing any cell. Duplicate cell edits, sparse arrays, and malformed patches reject atomically. A changed batch increments revision once; an empty or unchanged batch does not increment it. Blocking a cell retains its cost until you change it. Clear both explicitly with { blocked: false, cost: 1 } when that is the intended edit.
Routes are detached values, not live objects. Terrain edits do not automatically recompute old routes or stop an actor. Your game decides when to replan and how to move safely. A revision is local to a grid's history; loading an older snapshot or constructing a new grid can reuse an old revision number. It is not a globally unique map identity.
World coordinates
const cell = garden.worldToCell({ x: pointerWorldX, z: pointerWorldZ });
if (cell) {
const center = garden.cellToWorld(cell, 1.05);
console.log(center); // plain { x, y, z }
}
const route = garden.findWorldPath(
{ x: courier.position.x, z: courier.position.z },
{ x: destination.position.x, z: destination.position.z },
);Pass plain XZ records, not THREE.Vector3 instances. A world search maps its endpoints to containing cells; returned points are cell centers, not the exact input endpoints. Y is a chosen output height and does not affect navigation.
Saves
const text = JSON.stringify(garden.snapshot());
const restored = Wayfinder.fromSnapshot(JSON.parse(text));
garden.restore(JSON.parse(text)); // atomic; config must match this instanceVersion 1 saves contain the complete normalized grid configuration, revision, and a dense row-major array of blocked flags and weights. Loading validates version, dimensions, limits, exact fields, and every cell before replacing the current terrain. Inputs and outputs do not expose mutable internal arrays. fromSnapshot() creates a grid from the saved configuration; restore() requires the current configuration to match. There is no automatic schema migration, merging, storage, or authentication. Snapshot validation checks structural validity, not whether an untrusted player earned a terrain change.
Practical limits
| Item | Bound |
|---|---|
| Width / height | Integers 1–128 each |
| Total cells | At most 16,384 |
| Cell size | Finite 0.001–1,000 world units |
| Origin X/Z and output Y | Finite −1,000,000 to 1,000,000 |
| Traversal weight | Finite 1–1,000, including fractional values |
| Batch edits | At most one edit per grid cell |
| Search budget | 1–cell count expanded cells; default cell count |
| Revision | Safe integer; exhaustion throws before terrain changes |
Search uses O(N) bounded working storage and O(N log N) worst-case heap work for N cells on this constant-degree grid. It runs synchronously; repeated maximum-size searches can still block a UI thread. The maximum is a validation limit, not a frame-time guarantee. Put larger workloads behind an application-owned worker if needed. The package stores no route cache or search history. The demo displays 120 cells and rejects uploaded files larger than 2 MB.