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:
- Particles. If
particles > 0andshipexists, emitParticles.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. - Micro-SFX.
MicroSfx.play(event)is called regardless ofparticles— even rows withparticles: 0exist 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)
| Event | Particles | Category |
|---|---|---|
player_shot | 0 | combat / offense |
kill | 0 | combat / offense |
enemy_kill_tiny | 0 | combat / offense |
enemy_kill | 0 | combat / offense |
enemy_kill_large | 3 | combat / offense |
elite_kill | 10 | combat / offense |
boss_hit | 5 | combat / offense |
boss_kill | 30 | combat / offense |
player_hit | 0 | combat / defense |
player_hull_hit | 0 | combat / defense |
shield_hit | 0 | combat / defense |
shield_break | 20 | combat / defense |
shield_broken | 20 | combat / defense |
invuln | 15 | combat / defense |
revive | 25 | combat / defense |
levelup | 0 | progression |
warp_collect | 20 | progression |
beacon_done | 20 | world events |
event_done | 12 | world events |
solar_flare | 20 | world events |
extraction_done | 20 | world events |
chase_catch | 15 | world events |
detected | 8 | world events |
comet_catch | 0 | pickups |
crate_break | 0 | pickups / crates |
crate_break_big | 0 | pickups / crates |
ram_hit_light | 0 | environment / ram |
ram_hit_medium | 0 | environment / ram |
ram_hit_heavy | 2 | environment / ram |
object_hit | 0 | environment |
object_destroyed | 20 | environment |
aoe_pulse | 10 | AoE / death |
player_death | 25 | AoE / death |
Tuning patterns
- Counts cluster at the extremes. Roughly half the table is
0(audio-only triggers) and the rest is concentrated at2–3(small chips),5–15(medium beats),20–30(climactic moments). There are no intermediate values like1,4, or17— 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 haveparticles: 0. Their job is to keepMicroSfx.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_breakandshield_brokenare 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
MicroSfxcue (or accept silence). Both lookups happen unconditionally insideJuice.fire. - Particle count
0is the right answer when only audio should fire — it’s not a placeholder for “TODO add particles.”
Cross-references
- juice-system — the
Juice.firedispatcher, 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 paralleleventName → stingmap; same keys are expected here.ShipRecoil(same file asJUICE_TABLE) — cosmetic squash/stretch animator. Co-located but unrelated — it does not route throughJUICE_TABLE.