Spawn Arc / Velocity-Cone Bias

Enemies always spawn off-screen at a distance from the ship, with the spawn ring biased toward the player’s recent movement direction so the action stays ahead of motion rather than behind it.

Off-screen spawn band

Spawns are constrained to a ring around the ship:

  • Minimum distance: 450pxMIN_SPAWN_DIST = 450 is enforced inside spawnEnemy(). Any candidate point closer than 450px is pushed outward along its bearing so nothing pops in on top of the player. See engine/enemies/spawner.ts line 929.
  • Early-trickle band: 400–550px — the first-60s “guaranteed close-range” trickle picks 400 + rand * 150. These are the cosy, just-off-screen spawns that anchor opening pressure. Line 550.
  • Directional base distance: 350 + speed lookahead_spawnDirectional() adds min(750, speed × 1.5) to a 350px base, capped at 1100px so spawns always land inside the 1500px prune boundary. Per-pack centre then jitters by ±150px, so the realised band reaches roughly 400–1250px (slow ships) up to ~1250–1400px equivalent for fast ships near the cap. Lines 718–747.

The 1500px prune boundary outside this range matters: anything that drifts past it gets recycled, so spawning further out would just feed the pool. The cap is deliberate.

Velocity-cone bias

The direction the player is moving — not their facing — drives the spawn arc.

  • Heading source: 5-second position-sample window. The spawner stores the ship’s (x, y) every frame and uses the displacement from oldest sample → current position as moveAngle. This gives the true average heading and is immune to momentary stick jitter or twitchy strafe. If displacement < 10px (player is essentially stationary), it falls back to ship.angle. Lines 707–716.
  • 70 / 30 split. For each spawn pack, Math.random() < 0.7 selects “ahead” mode; the remaining 30% pick a fully random bearing around the player. Line 738.
  • Cone width: ±π/4 (45° each side, 90° total). “Ahead” spawns roll moveAngle + (rand - 0.5) × π × 0.5. So most spawns appear inside a 90-degree wedge centred on the direction of motion. Line 741.
  • Pack coherence. Every enemy in a pack shares the same spawn angle and a pack centre, then individuals scatter 30–60px around that centre. So a single roll seeds 2–4 (and up to ~6 in late game) clustered enemies in roughly the same place rather than dispersing them around the ring.

The bias is the reason that holding a direction “carries” the player into fresh enemies — you’re not running away from spawns, you’re running into them. The 30% random fraction keeps you from being able to perfectly outpace pressure by sustained drifting.

Spawn-grace window

Each planet defines a spawnGraceSeconds value (data/planet-config.ts). The spawner reads PLANETS[world.planetId]?.spawnGraceSeconds ?? 1 at line 451.

  • Default: 1 second. Most planets use a 1s warm-up where no spawns fire. After grace ends, the early-trickle phase begins.
  • The Void (planet 3): 0 seconds. spawnGraceSeconds: 0 skips the warm-up entirely — full pressure from t=0. The grace branch (if (planetGrace === 0) { graceRamp = 1; }) short-circuits to maximum ramp on frame 1.
  • Grace ramp shape (when grace > 0):
    • 0 → planetGraces: graceRamp = 0 (no spawns)
    • planetGraces → 30s: graceRamp = 0.55 (flat at 55% of normal pressure)
    • 30s → 60s: linear interpolation from 0.55 → 1.0
    • 60s+: full pressure

So the “first N seconds” window is really three nested phases:

  1. Hard grace (0 → 1s typical): nothing spawns.
  2. Flat ramp (1 → 30s): close-range trickle only, ~55% pressure.
  3. Growing ramp (30 → 60s): pressure climbs to full while still in trickle mode.

After 60s, the directional pack-spawn system takes over from the close-range trickle, and the velocity-cone bias becomes the dominant placement model for the rest of the run.

Why it works

The velocity cone is what makes Starship Survivors feel like a bullet hell with thrust rather than a static survivor arena. If spawns were uniformly random around the ship:

  • Drifting in any one direction would empty the screen ahead.
  • 70% of the threat would be behind you and irrelevant.
  • Fast ships would coast through dead space.

The cone makes motion produce engagement. The 30% random tail prevents the cheese of stationary or constant-direction play. The 5-second sample window prevents the cheese of high-frequency direction flicks “fooling” the spawner.

See also