Burst Fire Behavior

The burst_fire BulletBehavior models multi-shot bursts (burst rifle, revolver) as an invisible coordinator projectile that spawns short-lived hitscan sub-shots at a locked target on a cadence. The outer projectile never renders — it exists only to drive timing, target lock, and aim. When the burst is exhausted, the coordinator despawns and the weapon’s normal cooldown takes over.

Registered in src/starship-survivors/engine/weapons/bullets.ts (BulletBehaviors.register('burst_fire', ...)).

The coordinator projectile

When the fire pipeline resolves a burst-fire weapon, it spawns a single “outer” bullet carrying the burst state. This bullet:

  • Does not render. It has no arch rendering shape that draws to canvas. Visuals come exclusively from the sub-shot beam traces.
  • Follows the ship. Every update, b.x = ship.x; b.y = ship.y. The coordinator is glued to the ship’s position — the ship moves, the next sub-shot fires from the new origin.
  • Carries the burst state. All countdown timers, remaining shot count, locked target reference, aim parameters, and per-sub-shot damage/range/width live on _burst* fields of the coordinator.
  • Has a long lifetime. While _burstShotsLeft > 0, the coordinator’s lifetime b.l is clamped to max(b.l, 0.5) each frame, so it survives long enough to finish the burst regardless of normal lifetime decay.

Sub-shot cadence

Each frame the coordinator decrements _burstTimer by dt. When _burstTimer <= 0 and shots remain:

  1. _burstShotsLeft--.
  2. _burstTimer resets to _burstIntraSec (the intra-burst interval, default 0.045s ≈ 22 sub-shots per second).
  3. One hitscan beam-trace bullet is spawned at the ship’s current position.

The 0.045s default produces a tight rip — three to five shots feel like one trigger pull. Weapons override _burstIntraSec to slow or quicken the cadence.

Target lock and aim

The coordinator locks onto a single target for the duration of the burst — the ship doesn’t re-acquire between sub-shots.

Acquisition (only when no manual fire and no current target):

  • Scan world.enemies for the closest enemy within _burstAcquireRange (defaults to _burstBeamRange, then 220px).
  • Skip enemies that are dead, _frozenForLag, _dying, or still in the 0.15s spawn-immunity window (_spawnT > 0.15 excludes mid-spawn enemies).
  • Store the winner in b._burstTarget.

Re-acquisition triggers: the locked target dies, freezes for lag, or enters dying state mid-burst. The coordinator re-runs the scan on the next sub-shot.

Aim per sub-shot:

  • Vector from ship to target → atan2 for aimAngle.
  • Beam range = distance + target.radius + 30px so the hitscan line always passes through the enemy (no “beam ends just short” misses).
  • Apply ±1.5° random jitter (0.052rad total spread) for visual variety. The buffer ensures jitter doesn’t cause misses.

If _manualFire is set, the coordinator skips auto-acquisition entirely and uses whatever _burstAimAngle the manual-fire system supplied.

Sub-shot bullet shape

Every sub-shot is a fresh entry in world.playerBullets configured for one-tick hitscan:

FieldValueWhy
vx, vy0, 0Hitscan, not a moving projectile.
dmg_burstDmg || b.dmgPer-sub-shot damage. Total burst damage = dmg * shots.
pierceCount999The beam line passes through every enemy on its path.
rad_burstBeamWidth || 2.2Beam visual width, also drives the hit detection slab.
l, maxLifetime0.06Lives for ~3.6 frames — long enough for one collision pass.
_collisionMode'beam_trace'Engine treats this as a hitscan line, not a circle.
_beamRangedist + radius + 30Computed per shot from locked target.
_beamAngleaimAngle + jitterAim direction with jitter applied.
_behaviors[]Sub-shots inherit none of the coordinator’s behaviors — they’re dumb beam traces.

The 100-bullet cap (world.playerBullets.length < 100) protects against runaway spawn in pathological cases.

Despawn

After _burstShotsLeft hits zero, the coordinator clamps its lifetime to min(b.l, 0.08) — it survives ~5 more frames so the last sub-shot can render and resolve, then naturally expires.

Why a coordinator, not a fire-loop?

The fire pipeline runs once per weapon cooldown. A burst weapon needs N shots over ~150ms — far shorter than its cooldown, far longer than one frame. The coordinator pattern lets the burst run inside the simulation as a normal bullet update, so it:

  • Pauses naturally with the world (no separate timer subsystem).
  • Tracks the moving ship without extra plumbing.
  • Holds target lock across frames without leaking state into the weapon definition.
  • Cleans up via the existing bullet-lifetime path — no special despawn case.