Boss Spawn Profiles

A boss spawn profile is the timeline of pressure adds that runs alongside a boss encounter — answering what / how many / when / where / how often. The data shapes live in src/starship-survivors/data/spawn-profiles.ts; the runtime that ticks the active profile and fires waves lives in src/starship-survivors/engine/enemies/boss-spawn-profile.ts.

Pressure adds spawned by a profile are normal enemies — they drop XP, fire enemy_kill, count toward streak, and do not contribute to the boss bar (they never set sharesHealthWithBoss). The profile only describes the schedule; the actual spawn lands through the standard GameMaster.spawnEnemy path so all the usual enemy systems apply.

Lifecycle

  • game.bossSpawnProfile holds the active profile id while the encounter is live (or null / 'none' when there is none).
  • game.bossEncounterTime is the encounter clock the runtime advances every tick.
  • resetBossSpawnProfile() is called at encounter start, encounter end, and on player death, so a fresh encounter never inherits stale fire counts from a previous one.
  • An unknown profile id throws — there is no silent fallback.

See spec §3.4 and §6 (spawn profile lifecycle).

The six profiles

ProfileRoleWave summary
noneNo pressureEmpty waves array — boss fights solo.
light_pressureBackground harassmentorb_common from arena edge every 8 s, starting at t=4 s.
heavy_pressureConstant gauntletgunner_common on cardinals every 12 s, starting at t=2 s. So the player never gets a clean window on the boss.
beacon_clearersAnchor-clearing interrupterscharger_common opposite the player every 15 s, starting at t=10 s. Fast melee that punish you for ignoring them.
stormBurst panicgunner_common on a ring every 20 s, starting at t=15 s. Long quiet windows, then a wall of mid adds.
crescendoPhase-tied escalationt=0: 2 cardinal gunners → phase 1: 4 ring gunners → phase 2: 6 ring gunners. Add count climbs as the boss crosses HP thresholds.

Wave structure

Every profile is { id, waves[] }. A wave is:

{
  trigger,           // when it fires
  enemyTypeId,       // what to spawn
  count,             // how many
  position,          // where, relative to arena
  affixIds?,         // optional affix stamp on each spawn
  abilityIds?,       // optional abilities granted to each spawn
}

Triggers

Three trigger kinds, all evaluated per wave per tick:

  • time{ kind: 'time', at }. Fires once when bossEncounterTime >= at. Tracked in a one-shot set.
  • recurring{ kind: 'recurring', every, from?, until? }. Fires every every seconds inside the optional [from, until] window. The runtime computes how many fires are due with floor((now - from) / every) + 1 but only steps one fire per tick, spreading spawn cost across frames if a long pause caused many fires to come due simultaneously.
  • phase{ kind: 'phase', index }. Fires once when any sharesHealthWithBoss enemy in the world reports phaseIndex === index. Phase index is stamped on the host by the respawn_as affix when the boss crosses an HP threshold.

Phase index convention

phaseIndex starts at 0 for the spawn body — so the initial boss host is phase 0. Crossing thresholds advances the index: a boss declaring two thresholds (e.g. Awakened Mech at 0.66 and 0.33) yields phase indices 1 and 2. A crescendo wave keyed to { kind: 'phase', index: 1 } therefore fires on the first threshold, not at encounter start.

Position strategies

All positions route through arena helpers so pressure adds cannot spawn outside the active encounter footprint (spec §2.6 / §3.4 “spawn safety”):

PositionWhere
cardinalCardinal pads at 0.7× arena radius. Wraps if count exceeds 4.
ringEvenly-spaced ring of N points at 0.7× arena radius.
randomN independent random points inside the arena.
edge_randomEvenly-spaced perimeter points rotated by a random offset, so successive waves don’t land on the same spots.
opposite_playerMirrored across arena center at 0.85× radius, clustered with 30–60 px angular jitter.

Affixes and abilities

affixIds and abilityIds are optional per wave. When present, each spawned enemy is stamped with enemy.affixes = [{ defId, state: {} }] and enemy.abilities = [{ defId, cooldownRemaining: 0, state: {} }] after spawn, on top of whatever the base enemy type already carries.

Key facts

  • Data: src/starship-survivors/data/spawn-profiles.ts — six profiles, all enemy ids reference the standard registry.
  • Runtime: src/starship-survivors/engine/enemies/boss-spawn-profile.ts — per-wave id is <profileId>#<index>; module state is reset at encounter boundaries.
  • Pressure adds never share health with the boss.
  • Phase index 0 is the body; phase wave triggers fire on indices ≥ 1.
  • Recurring waves only step one fire per tick to spread cost.
  • Unknown profile id is a crash, not a no-op.