Bullet archetypes
What this is
A bullet archetype is a short string tag attached to every spawned projectile that selects which visual branch the renderer draws for that bullet. It is purely a cosmetic / VFX routing key — the gameplay-side family field still drives the fire pipeline, collision, scaling, and damage. The archetype only tells drawBullet which if (arch === ...) branch to take.
The tag is stored on the weapon spec as the optional field bulletArchetype: string on WeaponCoreSpec. When a weapon fires, the fire pipeline copies that string onto each spawned bullet entity as barrel.archetype. When the renderer iterates world.playerBullets, it reads b.archetype and routes to the matching draw branch. Two unrelated weapons can declare the same bulletArchetype and share the same visual, decoupling visual identity from weapon balance — Coilgun reuses Rifle’s pulse_ball, Magnetar reuses Line’s tesla_line, Fire Ring reuses Sweep’s sweep_laser, and the bullet+fire legendary Trailblazer reuses Rifle’s pulse_ball too.
When bulletArchetype is omitted on a weapon, the fire pipeline falls back to a context-appropriate default:
| Fire path | Fallback archetype |
|---|---|
| Standard projectile spawn | The weapon’s family string (projectile, explosive, sniper, beam, chain, homing, sweep) |
| Arc-mortar spawn (cannon, mortar) | mortar_shell |
| Tesla-line spawn (line family) | tesla_line |
| Shield-arc spawn (barrier family) | shield_arc |
Bullets that arrive at the renderer with an archetype string that doesn’t match any branch fall through to the final else clause and draw as a colored circle with a white center.
The archetype registry
There is no central registration list — the renderer is the registry. Each archetype is implemented as one branch of the if / else if chain inside drawBullet in src/starship-survivors/engine/rendering/draw.ts, and a string only “exists” as an archetype if a matching branch exists there. Adding a new archetype means adding a new else if (arch === 'newname') branch and pointing one or more weapon specs at it.
The branches currently implemented:
| Archetype | Visual silhouette | Used by |
|---|---|---|
pulse_ball | Circular energy orb with white-hot core | rifle, coilgun, lgd_incendiary_rifle (Trailblazer) |
cannonball | Oversized black iron ball with orange glow + white outline + specular highlight | cannon |
projectile | Elongated cyan capsule with white highlight | family fallback for projectile weapons without an explicit archetype |
explosive | Chunky dark shell with orange nose cone | family fallback for explosive weapons |
sniper | Full-range beam with layered electrical glow, muzzle flash, impact flash, glitch lines, ghost-scar afterimage | family fallback for sniper weapons (railgun, death ray) |
beam | Pulsing multi-layer beam with solid core | family fallback for beam weapons |
chain | Blue electric orb with eight crackling lightning arcs | family fallback for chain weapons |
homing | Prebaked rocket sprite (red tip, silver body, black wings) + warm halo | family fallback for homing weapons (missiles) |
mortar_shell | Oversized fireball with red-orange glow, white outline, ground shadow, fake-3D arc height | arc-mortar fallback (mortar) |
blade / sweep_laser | Beam drawn from ship center along orbit angle with crackle, endcap orb, mini-arcs | sweep, fire_ring |
ember | Oversized teardrop pointing along travel, fully additive | flame family |
disc_ring | Boomerang plasma ring | disc |
shield_arc | Orbiting energy arc with thickness from _arcThickness | barrier |
tesla_line | Two orbiting balls connected by a thick electric line | line, magnetar |
mega_bullet | Slow brass-trail bullet body | lgd_autocannon (Mega Bullet) |
plasma_arc | Curved blue plasma wave | lgd_railslug (Wave Gun) |
quad_round | White machine-gun tracer | lgd_flak_rifle (4-Way Burst) |
phoenix_pulse | Continuous fire aura with phoenix outline | lgd_inferno (Phoenix) |
plasma_ball | Blue plasma mortar shell | lgd_arc_flame (Plasma Mortar) |
plasma_fire_zone | Persistent blue-fire damage patch | lgd_arc_flame ground zone |
fire_patch | Persistent fire trail damage zone | lgd_incendiary_rifle ground zone |
carpet_bomber | Screen-pinned bomber sprite | lgd_napalm_mortar (Carpet Bomber) |
star_halo_root | Ring-of-stars orbit formation | lgd_railstorm (Star Halo) declared as star_orbit |
beam_decay | Decaying beam sprite with cascade explosions | lgd_railstorm (Railstorm) variant |
artillery_strike | Telegraph circle + falling shell | lgd_blackhole_cluster (Hellrain) |
| fallback | Solid colored circle with white center | any unrecognized archetype string |
Weapons that explicitly declare bulletArchetype in their spec:
| Weapon | bulletArchetype |
|---|---|
| rifle | pulse_ball |
| coilgun | pulse_ball |
| cannon | cannonball |
| line | tesla_line |
| magnetar | tesla_line |
| barrier | shield_arc |
| disc | disc_ring |
| sweep | sweep_laser |
| fire_ring | sweep_laser |
| lgd_autocannon (Mega Bullet) | mega_bullet |
| lgd_railstorm (Star Halo) | star_orbit |
| lgd_flak_rifle (4-Way Burst) | quad_round |
| lgd_railslug (Wave Gun) | plasma_arc |
| lgd_incendiary_rifle (Trailblazer) | pulse_ball |
| lgd_arc_flame (Plasma Mortar) | plasma_ball |
Renderer behavior
The renderer consumes the archetype tag in one place: the drawBullet function. Per bullet, per frame, the renderer:
- Reads
b.archetypeoff the spawned bullet entity (the string copied from the weapon spec’sbulletArchetypeat spawn time, or the family/path fallback). - Scales the visual size by the camera zoom, a fixed
BULLET_VISUAL_SCALEfactor of 1.04, and — for legendary weapons (weaponId prefixlgd_) — a level-driven shrink factor from 0.15 at level 1 to 0.50 at level 20. - Optionally applies a 0.55 alpha multiplier for legendary weapons (further damping VFX intensity at high levels).
- Walks the
if (arch === 'pulse_ball') / else if (arch === 'cannonball') / ...chain and runs the matching draw branch, which handles its own composite operations, glow layers, sprite stamps, body shape, and overlays. - If no branch matches, draws the fallback solid colored circle with a white center.
- After the branch returns, runs a unified post-pass additive neon glow stamp around the bullet, except for archetypes that draw their own beam/long-form glow inline:
sniper,star_halo_root,beam_decay,artillery_strike,phoenix_pulse,plasma_fire_zone,fire_patch,carpet_bomber,mega_bullet. Forcannonball, the post-pass glow radius is multiplied by 1.8 to match its oversized body.
The renderer also gates a now-disabled universal glow halo on the archetype tag — a hardcoded skip list of sniper, orbit, gravity_mine, shield_arc, tesla_line, homing, mortar_shell prevents the halo from drawing on archetypes that already render their own beam, mine, or arcing visual.
Visual size, colors, and bullet phase data are read off the bullet entity itself (b.radius, b.c1, b.c2, b._arcHeight, b._beamAngle, b._beamWidth, b._orbitAngle, etc.). The archetype tag does not carry any data of its own — it is purely a switch key into the draw chain.
Reuse across weapons
The archetype-as-string design means visual identity is decoupled from weapon ID. Four explicit reuse cases ship today:
| Reusing weapon | Reuses archetype from | Tag |
|---|---|---|
| coilgun | rifle | pulse_ball |
| magnetar | line | tesla_line |
| fire_ring | sweep | sweep_laser |
| lgd_incendiary_rifle (Trailblazer) | rifle | pulse_ball |
In each case the reusing weapon has different damage tag, color palette, fire rate, family scaling, and gameplay role, but draws the same projectile silhouette as its donor weapon. The renderer reads b.c1 / b.c2 (the weapon’s color1 / color2) inside most branches so the same shape draws in different colors per weapon — Coilgun’s pulse_ball is violet, Rifle’s is silver-white, Trailblazer’s is orange.
Authoring a new weapon that should look like an existing one is a single-field change: copy the donor’s bulletArchetype value onto the new spec, set the new color palette, and the renderer requires no edits. Authoring a new visual identity requires adding both a new branch in drawBullet and a new tag string on the spec — the tag space is open and the renderer’s fallback-circle clause is the safety net for any spec/renderer drift.