What this is
burst_fire is a bullet behavior (registered in engine/weapons/bullets.ts) that turns one trigger pull into a timed sequence of hitscan sub-shots. The cast spawns a single invisible coordinator projectile; that coordinator stays glued to the ship and emits N short-lived beam-trace bullets at fixed intervals until its budget is spent.
It is selected by a weapon declaring behavior: 'burst_fire' in its WeaponCoreSpec, which routes its cast through WeaponManager.fireBurst in engine/weapons/weapons.ts.
| Field | Value |
|---|---|
| Behavior id | burst_fire |
| Bullet behavior priority | 5 |
| Coordinator archetype | burst_coord |
| Coordinator collision mode | beam_trace |
| Coordinator visible | No |
| Sub-shot collision mode | beam_trace |
| Sub-shot archetype | sniper |
| Sub-shot lifetime | 0.06 s |
| Sub-shot pierce | 999 |
| Player-bullet cap (guard) | 100 |
Weapons that declare behavior: 'burst_fire':
| Weapon | File |
|---|---|
| Revolver | data/weapons/revolver.ts |
The burst weapon (data/weapons/burst.ts) is named similarly but does not use this behavior — it is a single-cast multi-target hitscan that fires its shots all on the same frame via fireBeamTrace, not via the burst_fire coordinator.
How the coordinator works
fireBurst reads stats off the weapon spec and spawns one coordinator bullet with the burst budget packed into private fields. On every engine tick, the burst_fire update runs:
| Step | Action |
|---|---|
| 1 | Snap coordinator position to ship.x, ship.y |
| 2 | Decrement _burstTimer by dt |
| 3 | If _burstTimer <= 0 and _burstShotsLeft > 0: fire one sub-shot, decrement _burstShotsLeft, reset _burstTimer to _burstIntraSec (default 0.045 s if missing) |
| 4 | While _burstShotsLeft > 0: clamp b.l (lifetime) to a minimum of 0.5 s so the coordinator survives |
| 5 | When _burstShotsLeft <= 0: clamp b.l to a maximum of 0.08 s so the coordinator despawns shortly after the final sub-shot |
| Coordinator field | Source | Purpose |
|---|---|---|
_burstShotsLeft | burstPattern stepped-stat at rawLvl, plus getExtraProjectiles from more_projectiles upgrades | Sub-shot budget |
_burstTimer | Spawned at 0 | Triggers first shot on the very next tick |
_burstIntraSec | def.cadenceStepSec (or 0.045 s fallback) | Time between sub-shots |
_burstDmg | damage passed into fireBurst | Damage per sub-shot |
_burstAcquireRange | def.acquireRange at level, scaled by horizontal range upgrades | Target search radius |
_burstBeamRange | acquireRange * beamLengthMult | Hitscan trace length fallback |
_burstBeamWidth | def.beamWidth at level, fallback 2.2 | Sub-shot beam width |
_burstAimAngle | aimAngle from the firing pipeline | Initial aim, used when no auto-target |
lifetime | burstCount * intraSec + 0.12 | Coordinator hard expiry |
The coordinator carries behaviors: ['burst_fire'] and collisionMode: 'beam_trace' but it is invisible to the player. It does not deal damage on its own — only its emitted sub-shots do.
The locked-target rule
Auto-fire bursts use a single locked target for the entire sequence. The lock is established lazily on each sub-shot’s tick and held until that enemy dies or is frozen.
| Condition | Behavior |
|---|---|
_manualFire is set | Skip target acquisition; use the original _burstAimAngle for every sub-shot |
_burstTarget is null, dead, _frozenForLag, or _dying | Re-acquire: scan world.enemies, pick the closest alive non-spawning enemy within acquireRange |
Enemy still spawning (_spawnT > 0.15) | Excluded from target search |
_burstTarget is alive | Reuse it — do not re-scan |
When a locked target is available, the sub-shot aims at it directly. Aim is recomputed each sub-shot, so a moving target stays in the cone for the full sequence.
| Per sub-shot aim adjustment | Value |
|---|---|
| Aim angle | atan2(target.y - ship.y, target.x - ship.x) |
| Random jitter applied to aim | (rand - 0.5) * 0.052 rad (about ±1.5°) |
| Beam range used | dist_to_target + target.radius + 30 |
The +30 unit overshoot guarantees the beam trace passes through the enemy’s body even at the edge of the lock. If acquisition fails (no enemy in range), the coordinator skips firing this tick — the timer still resets, so the next opportunity comes one _burstIntraSec later.
Hitscan sub-shots
Each sub-shot is a short-lived beam_trace bullet, not a travelling projectile. It is pushed into world.playerBullets (subject to the 100-bullet cap) and resolves its damage via the engine’s beam-trace collision the same frame it spawns.
| Sub-shot field | Value |
|---|---|
x, y | ship.x, ship.y |
vx, vy | 0, 0 (no travel) |
dmg | _burstDmg (per-shot damage, not split) |
pierceCount | 999 |
rad | _burstBeamWidth (fallback 2.2) |
l / maxLifetime | 0.06 s |
homingStrength | 0 |
blastRadius | 0 |
arch | sniper |
_collisionMode | beam_trace |
_beamRange | shotRange (target distance + radius + 30, or _burstBeamRange if no target) |
_beamWidth | _burstBeamWidth |
_beamAngle | Recomputed per sub-shot from the locked target |
_behaviors | [] (sub-shots carry no behavior; coordinator owns the sequence) |
Cadence is governed by two weapon stats:
| Stat | Field on WeaponCoreSpec | Role |
|---|---|---|
| Sub-shot count per cast | burstPattern (stepped stat by level) | Plus getExtraProjectiles(weaponId, more_projectiles_count) |
| Sub-shot interval | cadenceStepSec | Falls back to 0.045 s if the weapon omits it |
Revolver’s tuning (from data/weapons/revolver.ts):
| Stat | Value |
|---|---|
burstPattern (sub-shots) | 6 at L1, 8 at L5, 12 at L10, 18 at L15, 24 at L20 |
cadenceStepSec | 0.06 s |
warmupSec | 0.25 s |
Because the sub-shots are hitscan and vx, vy are zero, projectile speed and homing strength on the weapon spec do not affect the burst itself — they affect the seeker projectiles spawned by other revolver mechanics, not the coordinator’s emitted beam traces.