Sub-Projectile Types
Legendary weapons spawn secondary bullets and AoE bursts that aren’t in the player weapon registry. They live inside bullets.ts (src/starship-survivors/engine/weapons/bullets.ts) as world.playerBullets.push({...}) calls fired from a parent behavior’s update / onDeath / onHit hook, or as delayed-detonation bursts scheduled through the spawnDelayedAoE / spawnFlameZone primitives in engine/effects/custom-handlers.ts. Each sub-projectile carries an arch: tag (rendered as that archetype) plus a _behaviors: [...] list that tells the bullet runner which handlers to drive it with. Damage credit still flows back to the parent weaponId for telemetry.
The pool is capped: every spawn site is guarded by if (world.playerBullets.length < 100) to keep the bullet array bounded. Drops past 100 are silently lost.
Bullet-pushed sub-projectiles (carry arch: + _behaviors:)
These are real Bullet records with their own lifetime, position, velocity, and damage. They flow through the standard collision resolver and renderer.
arch tag | Spawned by | Parent legendary | What it does |
|---|---|---|---|
plasma_fire_zone | plasma_mortar_land.onDeath (bullets.ts:2688) | Plasma Mortar (lgd_arc_flame) | Stationary radius zone, rad: 0, lives 3 s, ticks energy damage 4×/sec to any enemy in _zoneRadius (= 80 % of the shell’s blast radius). Per-tick dmg = ceil(parent.dmg × 0.10). Runs plasma_fire_zone behavior. |
plasma_droplet | plasma_mortar_land.onDeath (bullets.ts:2747) | Plasma Mortar (lgd_arc_flame) | Three small cyan plasma droplets thrown radially at 280 px/s within a 60° spread from impact. Lifetime 0.3 s, rad: 25, first-hit collision, ~20 % of main damage. Runs plasma_arc behavior for visuals. |
fire_patch | carpet_bomber.update per bomb drop (bullets.ts:2913) | Carpet Bomber (lgd_napalm_mortar) | Persistent ground fire patch dropped at every bomb landing site. Lives 2.5 s, ticks 5×/sec at _zoneRadius = 70 % of bomb blast radius. Per-tick dmg = ceil(bombDmg × 0.06). Runs fire_patch_zone behavior. |
emp_ring | empWave.onDeath (bullets.ts:790) | (Non-legendary EMP weapons — listed for completeness) | Expanding ring AoE, lives 2 s, growing to _empMaxRad = parent.blastRadius, deals 80 % of parent damage. Not currently used by any legendary; the slot lives in the same registry path. |
sniper | burst_fire.update per sub-shot (bullets.ts:746) | (Burst-fire base weapons) | Per-burst hitscan beam-trace bullet, lifetime 0.06 s, pierceCount: 999, _collisionMode: 'beam_trace'. The Mega Bullet legendary uses mega_bullet_trail instead and does not invoke burst_fire, so this arch is base-weapon only. |
nova_ring | periodicRing.update (bullets.ts:114) | (Periodic-ring archetype) | Stationary ring, no damage, 0.25 s life, 3× the parent’s radius. Used by non-legendary periodic-pulse weapons. |
The two zone-archetype bullets (plasma_fire_zone and fire_patch) are special: they have vx/vy = 0, rad: 0 (so the bullet-vs-enemy radius check never trips), and apply damage themselves inside their update handler via enemyGrid.query + damageEnemy. They are sub-projectiles only in the technical sense — really they are persistent damage zones riding the bullet pool.
Coordinator bullets and their virtual sub-projectiles
Some legendaries spawn one coordinator bullet that internally tracks many virtual “stars” or “stages” without each one being a real Bullet push. These don’t use arch: for the children — the parent owns the array.
- Star Halo (
lgd_railstorm,orbit_ringbehavior, bullets.ts:1802). The cast spawns a single coordinator bullet. Insideb._ringStarsList[]are 5–30 plain JS objects (one per “star”), each with its own{x, y, slotAngle, phase, contactCooldowns}. The coordinator drives all of them throughstream → spin → burst_out → explodephases. Per-star explosion damage usesb._ringStarDmg(= spec.damage). Spin-phase sweep damage = 25 % of that, with per-star × per-enemy cooldown of one revolution. - Railstorm (
lgd_plasma_launcher,beam_decaybehavior, bullets.ts:2069). The beam bullet stores_bdExplosionCount(6–14 by level viabeamDecayExplosions) and schedules them as rawAoeExplosion.spawn+ per-enemy damage loops over a 2 s decay window. No sub-bullets are pushed — each cascade is a position + radius + damage triplet evaluated inline. - Hellrain (
lgd_blackhole_cluster,artillery_rainbehavior, bullets.ts:2182). The bomb itself is one bullet with two phases (telegraph→fall). The aoe_finish damage on landing comes from the standardaoe_finishbehavior on the same bullet — not a new sub-projectile. - Carpet Bomber (
lgd_napalm_mortar,carpet_bomberbehavior, bullets.ts:2823). The bomber is the coordinator. Each bomb drop applies AoE damage inline (no sub-bullet for the explosion itself), then pushes onefire_patchzone bullet (above) and onespawnFlameZonepatch for the lingering-flames artifact stack.
Delayed AoE “echoes” (not bullets at all)
Several legendaries layer in a thematic “echo” — a small delayed burst that detonates after the main effect. These are pushed into a private _delayedAoEs[] array in engine/effects/custom-handlers.ts via spawnDelayedAoE(x, y, radius, damage, delay, c1, c2) and are ticked by tickDelayedAoEs(dt). They have no projectile body, no collision, no arch. On detonation they paint an AoeExplosion + impact ring and damage every enemy in radius once with distance falloff. The pool is hard-capped at DELAYED_AOE_MAX (oldest shifts out).
| Echo name | Parent legendary | Where spawned | Pattern |
|---|---|---|---|
| Cluster Bomblet | Hellrain (lgd_blackhole_cluster) | artillery_rain.update impact frame (bullets.ts:2265) | One delayed AoE at the landing site after 0.4 s. Radius = 50 % of bomb blast, damage = 30 % of primary AoE damage. |
| Sparkle Wake | Star Halo (lgd_railstorm) | orbit_ring.update explode phase (bullets.ts:2040–2041) | Two tiny 15 px pulses per exploding star, at +0.20 s and +0.40 s. Per-pulse dmg = ceil(starDmg × 0.04). |
| Lingering Scar | Railstorm (lgd_plasma_launcher) | beam_decay.onDeath (bullets.ts:2155) | Six sample points along the beam path, each fires twice (at +0.05 s and +0.25 s). Radius = max(20, beamWidth × 0.35). Per-pulse dmg = cascade damage × 0.08. |
| Bomb Pyre | Carpet Bomber (lgd_napalm_mortar) | carpet_bomber.update per bomb drop (bullets.ts:2910) | Calls spawnFlameZone(landX, landY, 50, dmg × 0.20, 0.8) — a global-pool flame zone rather than a delayed AoE, but conceptually the same “echo” slot. Stacks with the lingering_flames artifact. |
| Slipstream Wake | Mega Bullet (lgd_autocannon) | mega_bullet_trail.onDeath (bullets.ts:2283) | Implemented inline (no helper call) — does its own 40 × 12 px oval damage check behind the slug at expiry, 25 % of slug damage. Listed here because it occupies the same “thematic echo” design slot. |
Legendary → sub-projectile coverage
| Legendary | Real sub-bullets | Virtual coordinator children | Delayed-AoE / flame-zone echoes |
|---|---|---|---|
Mega Bullet (lgd_autocannon) | — | — | Slipstream Wake (inline oval) |
Hellrain (lgd_blackhole_cluster) | — | — | Cluster Bomblet |
Star Halo (lgd_railstorm) | — | 5–30 stars in _ringStarsList | Sparkle Wake × 2 per star |
Phoenix (lgd_inferno) | — | phoenix_pulse ring is a single coordinator | — |
4-Way Burst (lgd_flak_rifle) | — | Hitscan only (quad_burst dispatcher) | — |
Wave Gun (lgd_railslug) | — | — | — |
Trailblazer (lgd_incendiary_rifle) | — (flame patches handled by the fire_trail weapon system, not as sub-bullets) | — | — |
Railstorm (lgd_plasma_launcher) | — | 6–14 cascade explosions inline | Lingering Scar × 12 |
Carpet Bomber (lgd_napalm_mortar) | fire_patch × 6–18 (one per bomb) | 6–18 bombs (shellCount) | Bomb Pyre × N (one per bomb, via spawnFlameZone) |
Plasma Mortar (lgd_arc_flame) | plasma_fire_zone × 1–3, plasma_droplet × 3 (per shell) | 1–3 shells (shellCount) | — |
Why not just register them as weapons?
These archetypes never appear in the player-facing weapon registry because they:
- have no acquire/fire pipeline (they don’t pick targets or schedule next-shot timers — the parent does)
- have no level-up entry (their stats are derived from the parent’s
dmg,blastRadius, etc., at spawn time) - have lifetimes counted in fractions of a second, designed as cleanup VFX-with-damage rather than independent weapons
- collide using the same
_collisionMode+_canHitDestructiblesflags as a regular bullet, so they piggyback on the resolver instead of adding a new path
Bullet pool / archetype catalog details live in bullet-archetypes and bullet-pool-recycle. Zone-tick mechanics are detailed in plasma-mortar-zones, carpet-bomber-zones, and flame-zones.