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
archrendering 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 lifetimeb.lis clamped tomax(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:
_burstShotsLeft--._burstTimerresets to_burstIntraSec(the intra-burst interval, default0.045s≈ 22 sub-shots per second).- 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.enemiesfor the closest enemy within_burstAcquireRange(defaults to_burstBeamRange, then220px). - Skip enemies that are dead,
_frozenForLag,_dying, or still in the 0.15s spawn-immunity window (_spawnT > 0.15excludes 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 →
atan2foraimAngle. - Beam range =
distance + target.radius + 30pxso the hitscan line always passes through the enemy (no “beam ends just short” misses). - Apply
±1.5°random jitter (0.052radtotal 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:
| Field | Value | Why |
|---|---|---|
vx, vy | 0, 0 | Hitscan, not a moving projectile. |
dmg | _burstDmg || b.dmg | Per-sub-shot damage. Total burst damage = dmg * shots. |
pierceCount | 999 | The beam line passes through every enemy on its path. |
rad | _burstBeamWidth || 2.2 | Beam visual width, also drives the hit detection slab. |
l, maxLifetime | 0.06 | Lives for ~3.6 frames — long enough for one collision pass. |
_collisionMode | 'beam_trace' | Engine treats this as a hitscan line, not a circle. |
_beamRange | dist + radius + 30 | Computed per shot from locked target. |
_beamAngle | aimAngle + jitter | Aim 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.
Related concepts
- Bullet Archetypes — how
archdrives rendering. - Collision Modes — what
'beam_trace'does. - Manual Fire Mode — alternate aim path that bypasses auto-acquisition.
- Fire Pipeline — where the coordinator gets spawned.
- Spawn Immunity Window — why mid-spawn enemies are skipped.