hex-world
    Preparing search index...

    Class FlowField

    A single-source-set Dijkstra field: the cheapest cost from every cell to the nearest goal, plus the neighbour to step to next.

    This is the many-units-one-destination counterpart to findPath. A* costs one search per unit; a flow field costs one search per destination, after which every unit reads its next step in O(1) no matter how many of them there are or how far away they stand. For an army converging on a rally point, or a horde chasing one target, that is the difference between hundreds of searches a turn and one.

    The field is built by expanding outward from the goals, so when the search reaches cell X from an already-settled cell Y, the move a unit will actually make is X → Y. costFn is therefore called as costFn(X, Y) — the direction of travel, not the direction of expansion. Symmetric cost functions (the common case) can ignore this entirely; asymmetric ones (uphill costs more than downhill, one-way fords) get the right answer without doing anything special.

    Pass an array and every cell flows to whichever goal is cheapest from it, with the watershed between them falling out of the search. One field can serve "retreat to the nearest fort".

    Because the cost asked about is always the move out of a cell, a cell your cost function refuses to admit anyone into still gets a direction if it has a passable way out — a unit spawned or shoved onto a wall is given a route off it rather than being told it is stranded. Nothing routes through such a cell, since the step into it is still rejected.

    Dense typed arrays — 13 bytes per map cell, allocated once. compute reuses them, so re-targeting the field every frame allocates nothing.

    const field = computeFlowField(offsetToHex(rallyCol, rallyRow), moveCost, map);
    for (const unit of army) {
    const path = field.path(offsetToHex(unit.col, unit.row));
    if (path) unit.travel(path);
    }
    // Re-target each turn without reallocating.
    const field = new FlowField(map);
    field.compute(goals, moveCost, { maxCost: 40 });
    Index

    Constructors

    Properties

    width: number
    height: number

    Accessors

    Methods

    • Rebuilds the field for a new goal set, reusing the existing buffers.

      Cost is one Dijkstra sweep over the reachable area — the same work getMovementRange does for one unit, shared by all of them.

      Parameters

      • goals: HexCoord | readonly HexCoord[]

        One HexCoord or several. Out-of-bounds goals are ignored; with none left in bounds every cell reports unreachable.

      • costFn: MoveCostFn

        Movement cost, called as costFn(from, to) in the direction of travel. Return Infinity for impassable.

      • opts: FlowFieldOptions = {}

      Returns this

    • Cost from (col, row) to the nearest goal, or Infinity if unreachable.

      Parameters

      • col: number
      • row: number

      Returns number

    • true if a path from (col, row) to some goal exists within maxCost.

      Parameters

      • col: number
      • row: number

      Returns boolean

    • The direction index (0–5, indexing HEX_DIRECTIONS) of the next step from (col, row). Returns -1 at a goal and for unreachable cells — call costAt to tell those two apart.

      Parameters

      • col: number
      • row: number

      Returns number

    • Walks the field from from to the goal it flows to, returning the same shape findPath does — from first, goal last, both inclusive — so it can be handed straight to HexUnit.travel().

      Returns null if from is unreachable, and [from] if it is already a goal. Following the field is O(path length): no search runs here.

      Parameters

      Returns HexCoord[] | null

    • A normalised world-space (x, z) direction to steer toward, or null at a goal or on an unreachable cell.

      This is not just next converted to world space. It blends the directions of every neighbour the field descends into, weighted by how much cost each one saves, which is what makes a crowd read as a crowd: units crossing open ground aim at the true bearing rather than snapping to one of six axes, and a column meeting an obstacle splits around both sides instead of filing through a single hex. Edges the cost function rejects are excluded, so a cell whose cheap-looking neighbour sits across an impassable cliff edge does not steer into it.

      Costs ≤6 costFn calls — per unit per frame, not per cell.

      Parameters

      Returns { x: number; z: number } | null

      const v = field.flowVector(layout, offsetToHex(unit.col, unit.row));
      if (v) { unit.worldX += v.x * speed * dt; unit.worldZ += v.z * speed * dt; }
    • Visits every cell the field reached, in row-major order. Scans the whole grid — O(width × height) — so it belongs in debug overlays and field visualisations, not in a per-unit hot path.

      Parameters

      • fn: (col: number, row: number, cost: number, direction: number) => void

      Returns void