JUICE_TABLE

Static registry that maps each named gameplay moment to a particle count and (implicitly) a micro-SFX cue. Defined as Record<string, JuiceEvent> in src/starship-survivors/engine/vfx/juice.ts and consumed by Juice.fire(event) — see juice-system for the surrounding pipeline.

Shape

interface JuiceEvent {
  particles: number; // number of particles to emit at the ship
}
 
export const JUICE_TABLE: Record<string, JuiceEvent> = { ... };

Each row carries one number: how many spark particles to burst when that event fires. There is no per-event color, origin, lifetime, or volume — those are fixed by the dispatcher.

Dispatcher behavior

Juice.fire(event) does exactly two things for every keyed event:

  1. Particles. If particles > 0 and ship exists, emit Particles.burst(ship.x, ship.y, particles, 'spark', { r:200, g:220, b:255 }, 60). Origin is always the ship’s current world position, color is always the same light-blue spark tint, lifetime base is always 60.
  2. Micro-SFX. MicroSfx.play(event) is called regardless of particles — even rows with particles: 0 exist so audio fires on a consistent trigger.

Unknown events silently no-op (if (!j) return;). No throw, no warn, no fallback — adding a new juice event means adding a row to this table.

Full event roster (~30 events)

EventParticlesCategory
player_shot0combat / offense
kill0combat / offense
enemy_kill_tiny0combat / offense
enemy_kill0combat / offense
enemy_kill_large3combat / offense
elite_kill10combat / offense
boss_hit5combat / offense
boss_kill30combat / offense
player_hit0combat / defense
player_hull_hit0combat / defense
shield_hit0combat / defense
shield_break20combat / defense
shield_broken20combat / defense
invuln15combat / defense
revive25combat / defense
levelup0progression
warp_collect20progression
beacon_done20world events
event_done12world events
solar_flare20world events
extraction_done20world events
chase_catch15world events
detected8world events
comet_catch0pickups
crate_break0pickups / crates
crate_break_big0pickups / crates
ram_hit_light0environment / ram
ram_hit_medium0environment / ram
ram_hit_heavy2environment / ram
object_hit0environment
object_destroyed20environment
aoe_pulse10AoE / death
player_death25AoE / death

Tuning patterns

  • Counts cluster at the extremes. Roughly half the table is 0 (audio-only triggers) and the rest is concentrated at 2–3 (small chips), 5–15 (medium beats), 20–30 (climactic moments). There are no intermediate values like 1, 4, or 17 — counts are tuned in coarse bands.
  • Audio-only rows are intentional. player_shot, kill, player_hit, shield_hit, levelup, crate_break, crate_break_big, the light/medium ram hits, object_hit, comet_catch — all have particles: 0. Their job is to keep MicroSfx.play(event) on a consistent contract while the visible feedback comes from dedicated VFX systems (death VFX, shield shards, level-up rings, etc.).
  • Largest bursts gate climactic moments. boss_kill (30), player_death (25), revive (25), shield_break / shield_broken / warp_collect / event_done* / extraction_done / solar_flare / beacon_done / object_destroyed (20). These are the table’s emphasis tier.
  • shield_break and shield_broken are duplicated on purpose. Both rows exist with identical counts (20) so callers can fire either name and still get the same response — useful while older call sites and newer ones converge on one verb.

Authoring rules

  • Event names are snake_case strings keyed directly into the record — there is no enum or generated type.
  • One row per moment. If you need a different color, origin, or shape, do not add a Juice column for it — emit directly from a dedicated VFX system instead (boss death rings, shield shards, debris cascades, etc.). The whole point of this table is its narrowness.
  • New row → matching MicroSfx cue (or accept silence). Both lookups happen unconditionally inside Juice.fire.
  • Particle count 0 is the right answer when only audio should fire — it’s not a placeholder for “TODO add particles.”

Cross-references

  • juice-system — the Juice.fire dispatcher, ship-position spawn rule, and what Juice deliberately does not do (no shake, no hit-stop, no flash).
  • MicroSfx (engine/audio/micro-sfx.ts) — the parallel eventName → sting map; same keys are expected here.
  • ShipRecoil (same file as JUICE_TABLE) — cosmetic squash/stretch animator. Co-located but unrelated — it does not route through JUICE_TABLE.