Bullet Archetype Renderer
The bullet archetype is a string tag a weapon attaches to every projectile it spawns. It is the renderer’s branch key: when drawBullet runs each frame it switches on this string to pick a distinct silhouette, glow color, scale, and per-frame animation. Two weapons that share the same BulletBehavior (e.g. both pierce) can still look completely different because their archetypes route to different visual branches.
Where it lives in the data
The authored field is bulletArchetype on a weapon definition (src/starship-survivors/data/weapons/_types.ts):
bulletArchetype?: string;Each weapon data file sets one literal string. Sampled values in the live registry:
| Weapon file | bulletArchetype |
|---|---|
cannon.ts | cannonball |
rifle.ts | pulse_ball |
coilgun.ts | pulse_ball |
disc.ts | disc_ring |
line.ts | tesla_line |
magnetar.ts | tesla_line |
barrier.ts | shield_arc |
sweep.ts | sweep_laser |
fire-ring.ts | sweep_laser |
legendaries.ts | mega_bullet, star_orbit, quad_round, plasma_arc, pulse_ball, plasma_ball |
If a weapon omits bulletArchetype, the spawner falls back to the weapon family name (e.g. projectile, explosive, chain, homing, blade, ember). The renderer has dedicated branches for both archetype strings and family fallbacks.
How it reaches the bullet
fireWeapon in engine/weapons/weapons.ts builds a barrel config per shot:
archetype: def.bulletArchetype || def.family,A few specialized spawn paths bypass the field entirely and hard-code an archetype string into the barrel — 'mortar_shell', 'tesla_line', 'shield_arc', 'blade', 'ember', 'cone_beam', 'star_halo_root', 'beam_decay', 'artillery_strike', 'fire_patch', 'phoenix_pulse', 'carpet_bomber' (see lines 1521, 1603, 1724, 1805, 1895, 2030, 2500, 2574, 2644, 2724, 2751, 2808). Those weapons either don’t author a bulletArchetype or have it explicitly overridden because the visual is tied to a special spawn pathway, not the standard projectile barrel.
The shared bullet-construction step on line 2918 stores the string on the live bullet as the short field arch:
arch: barrel.archetype || 'projectile',Bullets also accumulate one-off archetype tags when behaviors spawn child projectiles inside engine/weapons/bullets.ts — examples include arch: 'nova_ring' from chain explosions, arch: 'emp_ring', arch: 'plasma_fire_zone', arch: 'plasma_droplet', and arch: 'fire_patch'. These never flow through bulletArchetype on the data side; they are minted at spawn time inside the behavior handler.
How the renderer reads it
The per-frame draw call lives in engine/bridge.ts (around line 6076):
const bArch = b.arch || 'projectile';
drawBullet(ctx, b.x, b.y, b.rad, b.c1, 1, bArch, b.c2 || '#ffffff', b.vx, b.vy, wid, b);drawBullet (engine/rendering/draw.ts from line 746) is one giant if/else if chain keyed on the arch parameter. Each branch draws a fully bespoke silhouette using ctx.save() / translate / rotate / layered fills. As of the verified commit the active branches are:
pulse_ball, cannonball, projectile, explosive, sniper, beam, chain, homing, mortar_shell, blade (also matches sweep_laser), ember, disc_ring, shield_arc, tesla_line, mega_bullet, plasma_arc, quad_round, phoenix_pulse, plasma_ball, plasma_fire_zone, fire_patch, carpet_bomber, star_halo_root, beam_decay, artillery_strike.
If arch doesn’t match any branch, no body draws — there is no generic fallback inside drawBullet’s dispatch (it relies on the spawner always defaulting to 'projectile').
After the per-archetype body, drawBullet runs a shared additive-glow pass at the bottom of the function — but the pass is skipped for the archetypes that own their own glow logic: sniper, star_halo_root, beam_decay, artillery_strike, phoenix_pulse, plasma_fire_zone, fire_patch, carpet_bomber, mega_bullet. cannonball gets a 1.8× glow-radius multiplier because the iron-ball silhouette is dark and needs a bigger hot halo to read against most backgrounds.
The 'cone_beam' archetype is the one true exception — bridge.ts short-circuits before drawBullet and routes those bullets to drawConeBeam, which uses the dynamic_cone_fire GLSL shader instead of Canvas 2D.
Distinct from BulletBehavior
Behaviors (the strings stored on b._behaviors and registered in engine/weapons/bullets.ts) drive what the bullet does — homing, chaining, exploding on death, spawning fire patches, applying status, etc. Archetypes drive what the bullet looks like. They are independent dimensions:
- A weapon with
bulletArchetype: 'pulse_ball'and ahomingbehavior renders as a glowing orb but flies like a missile. - Two weapons sharing the
chainbehavior can have completely different archetypes and look nothing alike. - The
cannonballarchetype renders a dark iron ball regardless of whether the underlying behavior is bouncing, piercing, or exploding.
The renderer never inspects _behaviors. The behavior layer never inspects arch. They meet only at the bullet object.
Color and scale inputs the archetype reads
Each branch combines the archetype-fixed silhouette with three per-bullet values:
b.c1— primary tint (passed ascolortodrawBullet). Set per-weapon viadef.color1.b.c2— secondary tint (passed ascolor2). Set per-weapon viadef.color2.b.rad— collision radius. The renderer derives visual size as(radius || 3) * camera.zoom * 1.04, then applies a legendary-only shrink curve (0.15× at L1 → 0.50× at L20) for any weapon whoseweaponIdstarts withlgd_.
That separation is why every weapon needs two color stops in its data file: the archetype defines the shape and how the colors are mixed, but the data file picks which colors get mixed.
Authoring rules
- Pick an existing archetype string if your weapon looks like one of them. The branches in
drawBulletare hand-tuned; reusing one is much cheaper than adding a new branch. - Add a new archetype only when the silhouette genuinely needs to differ. A new branch means new code in
engine/rendering/draw.tsplus a decision about whether the universal glow pass should skip it. - Set both
color1andcolor2. Most branches use the secondary color for outlines, nose cones, or highlights — a missingcolor2defaults to white, which looks wrong on most archetypes. - Don’t reuse
familyas an archetype unless the family already has a branch. The fallbackdef.bulletArchetype || def.familyonly produces a visible bullet when the family string matches an existingarch === '...'clause.
Source files
src/starship-survivors/data/weapons/_types.ts:306— declares the optionalbulletArchetype?: stringfield.src/starship-survivors/engine/weapons/weapons.ts:890, 1003, 1521, 1895, 2030, 2918— propagatesdef.bulletArchetypeonto the barrel/bullet asarchetypeand thenarch.src/starship-survivors/engine/weapons/bullets.ts— bullet behavior handlers; spawns secondary projectiles with hard-codedarchtags (nova_ring,emp_ring,plasma_fire_zone,plasma_droplet,fire_patch).src/starship-survivors/engine/bridge.ts:6049-6076— per-frame dispatch: readsb.arch, short-circuits forcone_beam, callsdrawBulletfor everything else.src/starship-survivors/engine/rendering/draw.ts:746—drawBulletswitch keyed onarch; lines 810-2400 are the per-archetype branches; lines 2400-2410 are the shared additive glow pass with the skip-list.src/starship-survivors/data/weapons/cannon.ts,rifle.ts,coilgun.ts,disc.ts,line.ts,magnetar.ts,barrier.ts,sweep.ts,fire-ring.ts,legendaries.ts— concretebulletArchetypeassignments.