Phaser AE

Lane control

Free packv3.0.0-3Logic

Parallel lanes of moving things with real spacing: road traffic, river logs and diving turtles, conveyor belts, queues that pile up at a barrier, and streams released at an entrance on a timer. You author the lanes as data; the pack moves them, keeps the gaps and answers whether the player is on a log or under a car.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 19, 2026

Lane control: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

laneControl

function laneControl(): Pack<{ laneControl: typeof rules; }>

Install pure lane motion, occupancy, edge policies, timed activity and replay.

Helpers

createLaneControl

function createLaneControl(definition: LaneField): Timeline<LaneField, LaneState, Advance>

The field as a command timeline: apply({ kind: 'advance', seconds }) (0.001 to 60 s, whole milliseconds; adjacent advances merge into one undo step up to 60 s), undo, redo, reset, snapshot() (a JSON-safe save of the definition and the commands) and a validated restore. state and definition are detached copies. Throws on an invalid field.

createLaneWindow

function createLaneWindow<T>(options:LaneWindowOptions, generate:(index:number)=>LaneRow<T>)

Own live row lifetimes independently of any camera or renderer, for endless crossing games. Wrapped rows share a global clock; released rows start their release schedule on activation. Focus never resurrects retired rows. New rows are built and validated before changing the live window, so generation failures are atomic. Returns focus(index) (move forward; throws on a step back), advance(ms) (0 to 60000 whole milliseconds), rows (detached LaneWindowRows in index order) and stats (anchor, time, live, created, retired, first, last). Throws Invalid lane window on options out of range, and whatever validateLaneField throws for a bad row.

laneIntervals

function laneIntervals(item: Pick<LaneItem, 'x' | 'width'>, length: number, boundary: LaneDefinition['boundary'] = 'wrap'): { left: number; right: number; }[]

Clip a wrapping physical item into visible intervals; its identity remains the same at the seam.

laneOverlap

function laneOverlap(item: Pick<LaneItem, 'x' | 'width'>, x: number, radius: number, length: number, boundary: LaneDefinition['boundary'] = 'wrap'): boolean

Whether a footprint centred at x with half-width radius overlaps the item's visible span or spans in a field of length; touching edges do not count.

lanePhase

function lanePhase(item: Pick<LaneItem, 'cycle'>, time: number): LanePhase

The phase of the item's cycle at time milliseconds; always 'surface' for an item without a cycle.

laneRuntime

function laneRuntime(definition: LaneField)

The bare simulation of a field, for code that owns its own clock: advance(ms) runs whole 10 ms ticks (0 to 60000 whole milliseconds a call, the remainder carried), and state and definition are live, not copies, so do not mutate them. Validates the field first (see validateLaneField). Use createLaneControl for undo, redo and saves.

laneSpacing

function laneSpacing(items: Pick<LaneItem, 'x' | 'width'>[], length: number, wrap: boolean): number

Check actual edge-to-edge gaps, including the periodic seam.

laneSupports

function laneSupports(item: Pick<LaneItem, 'x' | 'width'>, x: number, radius: number, length: number, boundary: LaneDefinition['boundary'] = 'wrap'): boolean

Whether one visible span of the item wholly contains the footprint from x - radius to x + radius (to within 1e-8), so something standing there is supported.

sampleWrappingLanes

function sampleWrappingLanes(d: LaneField, time: number): Record<string, LaneItem[]>

Every item's position at time whole milliseconds (0 to 24 hours), computed directly rather than simulated, for a field whose lanes all wrap: a planner can look ahead without running the clock. Throws when a lane does not wrap or the time is not a whole number of milliseconds in range.

validateLaneField

function validateLaneField(d: LaneField): void

Check a field against the pack's limits: length 2 to 1000, 1 to 32 lanes with unique ids, speeds of at most 100 units per second, gaps from 0 to the length, at most 128 items per lane with unique ids, valid widths and cycles, spacing at least the lane's gap, and releases only on non-wrapping moving lanes. Throws Invalid lane field, Invalid lane, Lane items overlap or violate minimum spacing or Invalid lane release schedule.

wrapLane

const wrapLane = (x: number, length: number)=> …

Wrap position x into 0 up to length (field units), rounded to nine decimal places so seam arithmetic is stable.

Functions

generateLaneControl

function generateLaneControl(o: { seed: number; boundary?: LaneDefinition['boundary']; difficulty?: 'easy' | 'medium' | 'hard'; lanes?: number; length?: number; }): { definition: LaneField; certificate: Advance[]; complexity: { patterns: string[]; counts: number[]; minimumGap: number; }; }

A demonstration fixture, not an authoring API: a seeded random field for the Lane Control lab and tests, with fixed kinds (car, crate, log), alternating speeds of 1.1 and -0.9, lanes 1 to 6 and length 12 to 30. Seeds change item counts, widths and release patterns; difficulty sets the gap and the release rate. The certificate is 30 s of quarter-second advances it checked the spacing over. Throws on controls out of range. Author a LaneField for a game.

Types

LaneCycle

type LaneCycle = { … }

Visible warning and recovery surround an explicit submerged interval, in whole milliseconds.

  • surface: number — Milliseconds the item is up and usable; a whole number from 500 to 60000.
  • warning: number — Milliseconds of visible warning before it submerges; a whole number from 100 to 10000.
  • submerged: number — Milliseconds spent under; a whole number from 100 to 10000.
  • rising: number — Milliseconds of recovery as it comes back up; a whole number from 100 to 10000.
  • phase: number — Milliseconds the cycle is already advanced at time 0; a whole number from 0 to 1000000.

LaneDefinition

type LaneDefinition = { … }

One lane: its items all move at the lane's single shared, signed speed, and keep at least gap apart.

  • id: string — Identifier unique within the field (lower-case letters, digits and hyphens, starting with a letter).
  • speed: number — Signed speed in field units per second: positive moves toward length, negative toward 0; at most 100 either way.
  • gap: number — Minimum edge-to-edge gap between items in field units, 0 to the field length; releases wait for it at the entrance.
  • boundary: 'wrap' | 'despawn' | 'stop' — What happens at the far edge. wrap: items leave one end and re-enter the other with the same identity and spacing. despawn: an item is removed once its whole width has left. stop: the leading item halts at the edge and the rest queue behind it, gap apart.
  • items: LaneItem[] — The items present at time 0, left edges inside the field, spaced at least gap (across the seam too when wrapping); at most 128.
  • release?: { /** Whole milliseconds between release attempts, 10 to 60000. A blocked attempt is skipped, never queued. */ every: number; /** The items released in turn, cycling (1 to 16 entries); each is an item without \id` or `x`. Released ids are `released-`. / pattern: Omit<LaneItem, 'id' | 'x'>[]; /* The most items the lane may hold at once, 1 to 128; an attempt at the limit counts as blocked. */ limit: number; }` — A timed release stream at the entrance, for a moving lane that does not wrap.

LaneField

type LaneField = { … }

A field of parallel lanes sharing one length. validateLaneField checks it.

  • length: number — Field length in units, 2 to 1000; wrapping lanes repeat with this period.
  • lanes: LaneDefinition[] — The lanes, 1 to 32 of them, with unique ids.

LaneItem

type LaneItem = { … }

One physical object in a lane, such as a car, a log or a crate.

  • id: string — Identifier unique within the lane; it stays the same as the item wraps. Released items are named 'released-'.
  • kind: string — Semantic label such as 'car' or 'log' for the consumer to interpret; an identifier-style string.
  • x: number — Position of the item's left edge in field units, from 0 up to the field length. In a non-wrapping lane the item must lie wholly inside the field at the start.
  • width: number — Extent of the item along the lane in field units; above 0 and at most the field length minus the lane's gap.
  • cycle?: LaneCycle — Optional activity cycle for items that sink and rise (diving turtles); absent means always on the surface.

LanePhase

type LanePhase = 'surface' | 'warning' | 'submerged' | 'rising'

Where an item is in its activity cycle: 'surface' (usable), 'warning' (about to submerge), 'submerged' or 'rising' (coming back up).

LaneRow

type LaneRow<T> = { … }

A generated absolute row: arbitrary game metadata and an optional moving lane.

  • value:T — The game's own data for the row (terrain, a safe strip, a river); structured-cloneable.
  • lane?:LaneDefinition — The row's moving lane, if it has one, with the window's length as its field length.

LaneState

type LaneState = { … }

The runtime state of a lane field: the clock, every lane's items and the release bookkeeping.

  • time: number — Milliseconds simulated; always a multiple of the 10 ms tick.
  • remainder: number — Milliseconds, 0 to 9, carried over towards the next 10 ms tick.
  • items: Record<string, LaneItem[]> — Current items by lane id, released items included.
  • next: Record<string, number> — By lane id, the time at which the next release is attempted; 0 for a lane without a release.
  • serial: Record<string, number> — By lane id, how many items it has released; it picks the next pattern entry and numbers the released item.
  • exited: number — Items that have wholly left the field in despawning lanes.
  • blocked: number — Release attempts skipped because the entrance was occupied or the lane was at its limit.

LaneWindowOptions

type LaneWindowOptions = { … }

Dimensions of a forward-only row window. Generation must be deterministic by absolute index.

  • length:number — Every row's field length in units, 2 to 1000.
  • behind:number — Rows kept behind the focus, 0 to 15.
  • ahead:number — Rows built ahead of the focus, 1 to 30; behind + ahead + 1 is at most 32.
  • start?:number — The first focus row index, 0 to 1e9. Default 0.
  • time?:number — The shared clock at creation, whole milliseconds. Default 0.

LaneWindowRow

type LaneWindowRow<T> = LaneRow<T> & { /** The row's absolute index, as passed to the generator. */ index:number; /** The lane's items now (empty for a row without a lane). */ items:LaneItem[]; }

Bounded streaming view; row indices and item identities do not change when old rows retire.