• One-tick cellular-automaton step over the supplied bitmap.

    Iterates the bitmap's sparse ChunkedBitmap.activeCells set — only cells that might have changed (or are known to have ongoing state like a fire timer or sand rest counter) are processed. The set is maintained automatically: every ChunkedBitmap.setPixel call adds the changed cell and its 8 neighbors so external carve / deposit / paint ops AND the sim's own swap-mutations keep activation propagating organically. Cells that didn't move and have no ongoing state drop out of the set and don't return until a neighbor's mutation re-activates them — once a world reaches steady state, step becomes a no-op.

    On the very first call the bitmap is scanned once (ChunkedBitmap.enableActiveCellTracking) to seed the set with cells placed before tracking was enabled.

    Within each tick the snapshot is processed bottom-up (y = H-1 → 0) so material falling from row y can't be re-processed in row y+1. Side preference is per-cell (goRight === (x is even)), so contiguous fluid blocks spread symmetrically instead of shifting en masse. The tick parameter flips a global L/R bias each call so a body of fluid alternates its preferred side, killing residual asymmetries.

    Five mobile fluid kinds are implemented; their behavior is parameterised over a single generic stepFluid helper.

    • 'sand' — falls straight down (density swap with any lower-rank fluid below), slides diagonally into pure air. No horizontal flow. Optionally settlesTo a static variant after settleAfterTicks stationary ticks (v2.2 bridge).
    • 'water' — falls straight down (density swap), diagonal into air, multi-cell horizontal flow into air. Spread per tick is Material.flowDistance (default 4); pools level off over ~width / flowDistance ticks.
    • 'oil' — like water but rank 3 (< water rank 4): can't displace water on a fall, so oil floats on water.
    • 'gas' — rises straight up (density swap), diagonal-up into air, horizontal spread into air. Bubbles up through liquids since gas rank 0 < liquid ranks.
    • 'fire' — stationary. Each tick: ignites the first adjacent flammable neighbor it finds (top, sides, bottom); the new fire cell starts at timer 0. Increments its own timer and dies (→ air) at the burnDuration threshold. Stays in the active set until it dies regardless of whether anything flammable is nearby.

    Density ranks for vertical swaps:

    gas (0) < air (1) < fire (2) < oil (3) < water (4) < sand (5)
    

    Static materials never swap (regardless of rank). Vertical swaps only happen between different ranks, with the heavier ending up deeper — for downward motion that's srcRank > targetRank, for upward motion srcRank < targetRank. Diagonal slides and horizontal flow are air-only — the swap bookkeeping stays single-cell (no mid-flight three-cell shuffle).

    Cost: O(N log N) per tick where N is the number of currently- active cells (the log factor is the snapshot sort that orders rows bottom-up). For a mostly-settled world the active set drops to zero and step returns immediately. For a continuous pour it scales with the moving cells, not the world dimensions.

    The bitmap is mutated in place. Affected chunks are dirtied via the bitmap's regular setPixel path, so a subsequent TerrainRenderer.repaintDirty() picks up the changes. Chunk- collider rebuilds are NOT triggered for fluid-only mutations because the rebuild path filters to static materials only — see chunkToContours / componentToContours.

    Parameters

    • bitmap: ChunkedBitmap

      The bitmap to step.

    • tick: number = 0

      Optional tick counter that controls L/R alternation. Pass an incrementing integer (e.g. frame counter) so the bias flips each call. Default 0 — fine for one-shot tests but produces a slight bias if called repeatedly without changing.

    Returns void