PURPOSE

Generates terrain-aligned curved lanes for Roadster (racer) enemy spawns on city-biome missions. Produces 4-8 lanes, each an array of waypoints sampled from quadratic bezier curves that follow terrain contours. Racers steer between waypoints at high speed, giving the impression of vehicles traveling along streets. Falls back to cardinal-direction straight lanes when terrain is too sparse to cluster.

OWNS

  • LaneWaypoint interface (x, y).
  • RacerLane type alias (array of LaneWaypoint).
  • generateRacerLanes(world) — public entry point, returns 4-8 RacerLane arrays.
  • Private helpers: _clusterTerrain, _findClusterAxis, _sampleBezier, _extendLane, _generateCardinalLanes.
  • Tuning constants: CHUNK_SIZE (1024), MIN_CLUSTER_SIZE (3), CLUSTER_RADIUS (200), LANE_OFFSET (60), WAYPOINT_SPACING (30), LANE_EXTENSION (800).

READS FROM

  • WorldState.terrain (from ../core/types) — array of placed terrain pieces with x, y coordinates. Source of cluster geometry.

PUSHES TO

  • Returned array of RacerLane[] is assigned by the caller onto world._racerLanes. The lanes module itself does not mutate world state.

DOES NOT

  • Does not spawn enemies. Racer entity creation lives in spawner.ts (_spawnRacerGroup).
  • Does not steer or update racers in flight. Movement is handled by racer behavior code.
  • Does not regenerate lanes per-frame. Lanes are computed once at mission start (and again on planet advancement); they are static for the duration of a planet.
  • Does not handle non-city biomes. Caller decides whether to invoke this function (see Entry points).
  • Does not validate that lanes avoid terrain — clusters define the lane axis, but lanes are offset from the cluster edge and may pass through other terrain.
  • Does not despawn or recycle lanes.

Signals

None. This module is a pure function over WorldState.terrain. No event emission, no subscription.

Entry points

  • generateRacerLanes(world) is called from engine/bridge.ts:
    • Once at world init when _initPlanet?.enemySet === 'city'.
    • Again on planet advancement when advPlanet?.enemySet === 'city' && !_isSealedKind.
    • For non-city or sealed planets the caller sets world._racerLanes = null instead of calling this.
  • Output is consumed by engine/enemies/spawner.ts in _spawnRacerGroup, which picks a random lane, spawns 3-5 racers staggered along the start of the lane, stamps each with _racerLane and _racerWaypointIdx, and orients them along the lane direction.

Pattern notes

  • Algorithm pipeline: cluster terrain by proximity (flood-fill within CLUSTER_RADIUS), find each cluster’s longest axis (most distant point pair), curve a quadratic bezier through that axis biased toward the cluster centroid, sample evenly-spaced waypoints, offset two parallel lanes per cluster (one each side, perpendicular to the axis), extend endpoints outward by LANE_EXTENSION so racers have run-up distance off-screen.
  • Spawn direction: lane direction is implicit in waypoint order. The first waypoint is the start (after _extendLane prepends an extension point), the last is the end. _spawnRacerGroup stamps racer.angle = atan2(next.y - wp.y, next.x - wp.x) using the next waypoint, so racers always face down-lane from their spawn point. Both sides of a cluster axis become separate lanes (side = -1 and +1), so racers on opposite-side lanes travel in opposite directions relative to the cluster.
  • Lane assignment: racers do not pick lanes individually. _spawnRacerGroup picks one random lane from world._racerLanes and spawns the entire 3-5-enemy group on that lane, staggered at waypoint indices 0, 2, 4, ... (capped at lane.length - 1). All members of a spawn group share the same lane reference (_racerLane).
  • Lane budget: loop breaks after 4 cluster-derived lanes to avoid clutter. If fewer than 4 lanes were produced, cardinal fallbacks top up to a maximum of 8 total.
  • Fallback: _generateCardinalLanes produces 4 straight lanes (two horizontal at y±80, two vertical at x±80) through the terrain centroid (or origin if no terrain), each 4000px end-to-end. Used when terrain.length < MIN_CLUSTER_SIZE or when cluster-derived lanes underfill.
  • Arc-length sampling: _sampleBezier first estimates arc length with 20 uniform-t segments, then divides by WAYPOINT_SPACING to choose the final waypoint count (minimum 3). Waypoints are then evenly distributed in t, which approximates but does not guarantee even spatial spacing.
  • Bezier midpoint bias: the control point is (cx, cy) + ((cx, cy) - axisMid) * 0.3, pushing the curve 30% further past the centroid from the axis midpoint, so lanes bow outward through dense terrain regions rather than cutting straight across them.
  • Magic numbers: all tunings are named constants at the top of the file. CHUNK_SIZE is documented as needing to stay in sync with generation.ts but is not actually referenced inside this module’s current logic.
  • Loose typing: internal helpers use any[] for terrain arrays rather than importing the terrain type. Cluster members are accessed by .x / .y only.