How to design a new boss spawn profile

A spawn profile is the timeline of pressure adds that runs alongside a boss encounter. It answers what / how many / when / where / how often. The boss itself is unaware of the profile — profiles live in their own data file (src/starship-survivors/data/spawn-profiles.ts) and tick from their own runtime (src/starship-survivors/engine/enemies/boss-spawn-profile.ts). You wire a profile to a boss by name; the rest is data.

This page walks you through authoring a new profile end-to-end.

1. Start by cloning the existing profile closest to your intent

Six profiles ship today. Pick the one whose feel matches what you want, copy it, and tweak. Do not author from scratch.

Profile idFeelTrigger patternPosition
noneNo adds.(none)(none)
light_pressureBackground harassment — small adds dribble in from the arena edge.recurring every 8s from 4sedge_random
heavy_pressureConstant gauntlet — mid adds keep arriving so the player never gets a clean window on the boss.recurring every 12s from 2scardinal
beacon_clearersAnchor-clearing interrupters — fast melee that punish you for ignoring them.recurring every 15s from 10sopposite_player
stormBurst panic — long quiet windows then a wall of mid adds drops on the player.recurring every 20s from 15s, count 8ring
crescendoPhase-tied escalation — adds count climbs as the boss crosses HP thresholds.time at 0 + phase 1 + phase 2mixed

If your intent is “background nuisance,” clone light_pressure. If it’s “the room fills,” clone storm. If it’s “things get worse as the boss takes damage,” clone crescendo.

2. SpawnProfileDef structure

A profile is just an id plus an array of waves.

export interface SpawnProfileDef {
  id: string;
  waves: SpawnWaveDef[];
}
 
export interface SpawnWaveDef {
  trigger: SpawnTrigger;
  enemyTypeId: string;
  count: number;
  position: SpawnPosition;
  affixIds?: string[];
  abilityIds?: string[];
}

Each entry in waves is one independent rule. Triggers are checked every tick; the runtime decides when each rule fires. The id is the registry key (e.g. 'storm') and must match what the boss def references via spawnProfile.

3. Trigger options

See the boss-spawn-profile concept page for full runtime details. There are three trigger kinds.

TriggerShapeFires when
time{ kind: 'time', at: number }Once, when bossEncounterTime >= at.
recurring{ kind: 'recurring', every: number, from?: number, until?: number }Every every seconds, starting at from (default 0), optionally stopping at until.
phase{ kind: 'phase', index: number }Once, when any sharesHealthWithBoss enemy in the world reports phaseIndex === index.

A note on phases

Phase 0 is the boss body itself. The respawn_as affix is responsible for stamping phaseIndex on the host when the boss crosses an HP threshold. A boss that declares thresholds [0.66, 0.33] will produce phase indices 1 and 2 — so phase waves should target index: 1 or index: 2, never 0. Awakened Mech is the v1 user; copy that wiring.

4. Placement strategies

See the placement-strategies concept page for diagrams. Five options:

PositionBehavior
cardinalFour pads at N/E/S/W, at 0.7× arena radius. Counts above 4 wrap.
ringEvenly spaced around a ring at 0.7× arena radius. Good for “the room fills.”
randomAnywhere inside the arena. Chaotic feel.
edge_randomEvenly-spaced perimeter slots with a random rotation each wave (so consecutive fires don’t land in the same spots).
opposite_playerClusters spawn count around a point mirrored across arena center from the player, at 0.85× radius. Jitter ~30–60 units per add. Forces a movement decision.

All five route through arena.* helpers, so pressure adds cannot spawn outside the active encounter footprint.

5. Pressure vs role — adds are normal enemies

Pressure adds are NORMAL enemies. They:

  • Drop XP.
  • Fire enemy_kill events.
  • Count for streak / mission progress.
  • Do not share boss health (no sharesHealthWithBoss).
  • Do not contribute to the boss bar.

If you want a minion that ticks the boss bar down when it dies, that’s not a pressure add — that’s a sharing enemy stamped by the boss def, and it should not come from a spawn profile.

Choose enemyTypeId from existing common-tier types (orb_common, gunner_common, charger_common, etc.). Mid-tier adds at high counts will overwhelm low-level players; tune count against the enemy’s threat budget, not just its visual size.

6. Affixes and abilities (optional)

Waves can stamp affixes and abilities onto every enemy they spawn:

{
  trigger: { kind: 'recurring', every: 10 },
  enemyTypeId: 'gunner_common',
  count: 3,
  position: 'ring',
  affixIds: ['armored'],         // optional
  abilityIds: ['dash_lunge'],    // optional
}

The runtime stamps each affix as { defId, state: {} } and each ability as { defId, cooldownRemaining: 0, state: {} }. Test affix-stamping on a single wave first — if a normal enemy gains a boss-tier affix without the rest of the threat being tuned around it, the wave becomes a soft-lock generator.

7. Wire the profile to a boss

Add the new profile to SPAWN_PROFILES in data/spawn-profiles.ts, then reference its id from the boss roster’s spawnProfile field. The runtime reads game.bossSpawnProfile on encounter start and ticks waves from there until the encounter ends or the player dies (resetBossSpawnProfile clears state at both boundaries).

If you mis-name the profile id, tickBossSpawnProfile will throw unknown profile '<id>' — the runtime crashes loud rather than silently no-op.

Checklist before shipping

  • Profile id added to SPAWN_PROFILES.
  • Each wave has a sane enemyTypeId (exists in enemy registry).
  • Phase waves target index >= 1 (never 0 — that’s the boss body).
  • Recurring waves with no from start at encounter t=0; consider whether the player gets any clean window.
  • count × wave rate is sustainable against the boss’s threat budget.
  • Boss def’s spawnProfile field references the new id.
  • If using affixIds, the resulting enemy is not over-tuned for its tier.