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
LaneWaypointinterface (x, y).RacerLanetype alias (array ofLaneWaypoint).generateRacerLanes(world)— public entry point, returns 4-8RacerLanearrays.- 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 withx,ycoordinates. Source of cluster geometry.
PUSHES TO
- Returned array of
RacerLane[]is assigned by the caller ontoworld._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 fromengine/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 = nullinstead of calling this.
- Once at world init when
- Output is consumed by
engine/enemies/spawner.tsin_spawnRacerGroup, which picks a random lane, spawns 3-5 racers staggered along the start of the lane, stamps each with_racerLaneand_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 byLANE_EXTENSIONso racers have run-up distance off-screen. - Spawn direction: lane direction is implicit in waypoint order. The first waypoint is the start (after
_extendLaneprepends an extension point), the last is the end._spawnRacerGroupstampsracer.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.
_spawnRacerGrouppicks one random lane fromworld._racerLanesand spawns the entire 3-5-enemy group on that lane, staggered at waypoint indices0, 2, 4, ...(capped atlane.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:
_generateCardinalLanesproduces 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 whenterrain.length < MIN_CLUSTER_SIZEor when cluster-derived lanes underfill. - Arc-length sampling:
_sampleBezierfirst estimates arc length with 20 uniform-tsegments, then divides byWAYPOINT_SPACINGto choose the final waypoint count (minimum 3). Waypoints are then evenly distributed int, 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_SIZEis documented as needing to stay in sync withgeneration.tsbut 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/.yonly.