How to design a new boss

A sequential recipe for adding a boss encounter to Starship Survivors. A boss is one or more enemy “bodies” inside a sealed arena, anchored by a shared HP bar. Most new bosses are data-only: pick existing AI behaviors, wire one or more of the nine registered ability patterns, attach a spawn profile for add-pressure, and pick rewards. Custom AI or a new ability pattern is a separate, rarer task covered in step 9.

Before you start

Read this section first; it decides the whole shape of the work.

QuestionIf yesIf no
Does the encounter fit a single body, multi-body, or anchored composition you’ve seen in the existing roster?Data-only — write one .ts file under data/bosses/, register it in data/bosses/index.ts.You probably want a new AI behavior; that is a fork in engine/enemies/behaviors.ts.
Can the boss’s threats be expressed with the existing 9 ability patterns?Data-only — reference patterns by id, tune their params payload.New PatternId + fire fn in engine/abilities/patterns.ts.
Will the encounter use an existing spawn profile for pressure adds?Reference the profile id (e.g. 'crescendo', 'beacon_clearers').Add a new SpawnProfileDef in data/spawn-profiles.ts.
Is the boss the full final-tier encounter, or a level-roll mini-boss?Set kind: 'mini' (default — all currently shipped bosses) or kind: 'boss'.

Bosses are pulled from BOSS_DEFS (in data/bosses/index.ts) and assigned at run-roll time. pickRandomBossId('mini') and pickRandomBossId('boss') partition the pool by kind. The arena and roster are materialized by spawnBoss(defId, game, world, kind) in engine/boss/encounter.ts.

Step 1: Identity

Lock these decisions first. Each maps to a top-level BossDef field.

SlotFieldNotes
Stable ididUsed as the registry key. Lowercase snake_case. Examples: awakened_mech, hive_queen, reactor_core.
Display namedisplayNameShown on the HUD boss bar. Uppercase by convention.
Bar colorbarColorHex #rrggbb. Tints the HUD bar; per-body override is available in the roster entry.
Pool tagkind'mini' (default) — drawn from the mini-boss pool. 'boss' — drawn from the boss-tier pool.
Phase countimplicitPhase shifts come from the respawn_as affix on the anchor body (see step 4). A single-phase boss carries no respawn_as affix.
Theme / palette(data only)Palette colors are author-defined constants inside the boss file; they feed the VFX kit setup hooks.
Biome assignment(run-side)Bosses are not directly tagged to biomes in BossDef; the run sequence in data/level-progression.ts decides which boss rolls for a given level kind.

Place your new file at src/starship-survivors/data/bosses/<your-boss-id>.ts, export the BossDef const, then add the import + assignment in data/bosses/index.ts:

import { yourBoss } from './your-boss';
BOSS_DEFS[yourBoss.id] = yourBoss;

Step 2: Bodies and roles

The roster is an array of BossRosterEntry. Each entry produces count enemies at the given position. Bodies share the bar pool via sharesHealthWithBoss. Exactly one body is the bar anchor (isBoss: true). The anchor’s displayName is what shows on the HUD bar when the BossDef lookup falls through.

CompositionExampleHow it’s encoded
Single body, single phase(no shipped example — simplest case)One entry, count: 1, isBoss: true, sharesHealthWithBoss: true, no respawn_as affix.
Single body, multi-phaseAwakened MechOne entry with respawn_as affix in affixIds; affixState.respawn_as.phases lists thresholds and next typeIds.
Bar anchor + shielded anchorsHive QueenAnchor entry holds shielded_respawn; anchors are configured via affixState.
Bar anchor (untargetable) + sharing satellitesReactor CoreAnchor: isBoss: true, untargetable: true, sharesHealthWithBoss: false. Satellites: separate entry, isBoss: false, sharesHealthWithBoss: true.
Many-body swarmDigbot SwarmForeman entry (count: 1, isBoss: true) + swarm entry (count: 99, isBoss: false, both sharesHealthWithBoss: true).

Per-entry fields:

FieldTypeMeaning
enemyTypeIdstringRegistered enemy type that supplies sprite + base stats.
position'center' | 'cardinal' | 'ring' | 'random' | { x, y }Where the bodies spawn inside the arena. Object form is arena-relative.
countnumberBodies generated from this entry.
isBossbooleanMarks the bar anchor. Exactly one body should be the anchor.
sharesHealthWithBossbooleanDamage to this body drains the shared bar pool.
untargetablebooleanIf true, the body cannot be auto-targeted (used by Reactor Core’s Hauler).
aiIdstringMaps to a behavior. Allowed values: kite_player, hold_center, orbit_center, patrol_random, formation_lock, disperse_to_fill. Unknown ids crash at spawn.
affixIdsstring[]Affix ids (e.g. respawn_as, shielded_respawn, gated). Configured via affixState.
affixStateRecord<affixId, Record<string, unknown>>Per-affix initial state (anchor counts, phase thresholds, gate group ids, etc.).
abilityIdsstring[]Ability ids resolved against ABILITY_REGISTRY. Each becomes an AbilityInstance on the spawned body.
hpnumberPer-body HP. Multiplied at spawn by the kind multiplier and depth ramp.
barColorstringOptional per-body bar/outline color override.
displayNamestringOptional override for non-anchor bodies (inspection/debug labels).

AI id → behavior mapping (from mapAiIdToBehavior in engine/boss/encounter.ts):

aiIdBehavior
kite_playerkite
hold_centerstatic
orbit_centerorbit
patrol_randompatrol
formation_lockstatic
disperse_to_fillpatrol

Step 3: Phases and ability schedules

A multi-phase boss uses the respawn_as affix on the anchor. Each phase swaps the host enemy type (and its sprite) and replaces the active ability set, while preserving the shared HP bar.

affixState.respawn_as.phases shape:

KeyTypeMeaning
thresholdsnumber[]Sorted descending HP fractions of hpMax that fire the swap. Example: [0.66, 0.33] fires at 66% and 33%.
nextTypesstring[]Aligned by index with thresholds. Enemy typeId to swap into on each cross.
nextAbilityIdsstring[][]Aligned by index. Ability id lists for the new body.
nextAffixIdsstring[][]Aligned by index. Affix ids to carry into the new body. Include 'respawn_as' again on every non-terminal phase to chain further transitions.

Awakened Mech reference values:

PhaseThresholdNext body typeActive abilities
0 (spawn)industria_loadermech_phase1_aimed_volley
10.66industria_drillbotmech_phase2_spiral_vortex
2 (terminal)0.33industria_bigbotmech_phase3_cone_slam

Phase-1 ability example (registered at module init via registerAbility):

ParamValue
patternaimed_volley
cooldown2.0 s
telegraphMs800
count5
spread15°
bulletSpeed240
bulletDamage12
bulletRadius5

Phase-2 ability (sustained pattern):

ParamValue
patternspiral_vortex
cooldown8.0 s
bulletPerEmit2
emitIntervalMs100
rotationSpeedDegPerSec270
sustainMs4000
bulletSpeed180

Phase-3 ability:

ParamValue
patterncone_slam
cooldown3.0 s
telegraphMs1500
length300
halfWidth70°
damage35
bulletCount8

respawn_as fires BossDef.onPhaseChanged(host, newPhaseIndex, world, arena, kit) after the body swap. Use that hook to swap host-attached persistent VFX layers (the previous host died, so LayerHandles for the old host are dangling) and stage a transition cinematic.

Step 4: Abilities

Bosses fire abilities by id from ABILITY_REGISTRY. The registry is seeded from data/abilities.ts (static catalog) and grown at module init by per-boss registerAbility(def) calls. Each AbilityDef carries a pattern: PatternId plus a params: Record<string, AbilityParamValue> payload that the pattern fire fn consumes.

The nine registered patterns (from engine/abilities/index.ts and patterns.ts):

PatternTypeBehavior
radial_burstsingle-shot, telegraphedN bullets in 360° outward from caster. Telegraph paints a bloom around the caster.
aimed_volleysingle-shot, telegraphedN bullets toward the player’s locked-at-telegraph-start position; bullets fan across spread degrees.
cone_slamsingle-shot, telegraphedWedge AOE in front of caster; chunky bullets fan across the cone, traveling length distance.
telegraphed_circlessingle-shot, telegraphedM ground AOE circles painted at arena positions; detonate after telegraph.
beam_sweepsustainedLaser rotates from caster across an arc; deterministic per-frame ship-on-line damage.
spiral_vortexsustained, no telegraphBullets emit in a rotating Touhou-style spiral for sustainMs.
wall_sweepsingle-shot, telegraphedWall of bullets crosses arena with N safe gaps; alternates start side per fire.
spawn_telegraphsingle-shot, telegraphedPainted circles spawn enemies at the configured enemyTypeId after telegraph.
death_beamsustainedSingle arena-bisecting kill beam; instakill damage on the line.

Per-pattern params (cooldown is the time between fires once any sustain finishes):

PatternRequired paramsOptional params
radial_burstcount, bulletSpeed, bulletDamage, bulletRadiustelegraphMs (default 1000), bulletColor (default #ff5500)
aimed_volleycount, bulletSpeed, bulletDamage, bulletRadiusspread (default 0°), telegraphMs (default 800), bulletColor (default #ffaa00)
cone_slamlength, halfWidth (deg), damagebulletCount (default 8), telegraphMs (default 1200), bulletColor (default #ff3300)
telegraphed_circlescircleCount, circleRadius, circleDamagetelegraphMs (default 1500), arenaPositions (random / cardinal / ring, default random), bulletColor (default #ff8800)
beam_sweeparcDegrees, sweepDurationMs, damagePerSectelegraphMs (default 1000), beamLength (default arena radius), bulletColor (default #ff0066)
spiral_vortexsustainMs, emitIntervalMs, bulletPerEmit, bulletSpeed, rotationSpeedDegPerSecbulletColor (default #cc66ff)
wall_sweepwallSpeed, gapCount, gapWidth, bulletDamagedirection (horizontal / vertical, default horizontal), telegraphMs (default 2000), bulletColor (default #ff0044)
spawn_telegraphcircleCount, enemyTypeIdpositions (cardinal / ring / random, default cardinal), affixIds, telegraphMs (default 1500)
death_beamsustainMs, lineWidth, damageInstaKilltelegraphMs (default 3000), bulletColor (default #ff0000)

AbilityDef also supports startDelay (seconds) to delay the first fire after a body spawns. cooldown ticks start when the previous fire’s sustain phase ends, not at fire time.

Two registration paths:

PathWhen to use
Static catalog in data/abilities.ts (ABILITY_DEFS)Generic templates reused across enemies or bosses.
registerAbility(def) at module init inside the boss fileBoss-specific tuning that should live next to the boss data and not bloat the global table. Awakened Mech, Hive Queen, Digbot Swarm, Reactor Core all use this path.

Step 5: Spawn profile

The boss spawnProfileId selects a pressure-add timeline that runs alongside the encounter. Profiles live in data/spawn-profiles.ts. Each profile has an array of SpawnWaveDef entries; the runtime in engine/enemies/boss-spawn-profile.ts ticks them against game.bossEncounterTime.

Existing profiles:

Profile idTriggerWave
noneEmpty. Use when the boss’s own roster is the entire fight (e.g. Digbot Swarm).
light_pressurerecurring every 8s from t=4s3 × orb_common at edge_random
heavy_pressurerecurring every 12s from t=2s5 × gunner_common at cardinal
beacon_clearersrecurring every 15s from t=10s2 × charger_common at opposite_player
stormrecurring every 20s from t=15s8 × gunner_common at ring
crescendoone-shot t=0 + phase index 1 + phase index 22 / 4 / 6 × gunner_common (cardinal then ring then ring)

Trigger shapes (SpawnTrigger):

KindFieldsMeaning
timeat (seconds)One-shot at the given encounter time.
recurringevery, optional from, optional untilRecurring cadence inside an optional time window.
phaseindexFires when the anchor crosses into phase index N (driven by respawn_as).

Position values: cardinal, ring, random, edge_random, opposite_player.

To add a new profile: append a SpawnProfileDef to SPAWN_PROFILES and reference its id from the boss’s spawnProfileId.

Step 6: Scaling

Spawn-time scaling layers two factors onto each body’s base hp and damageMult:

FactorSourceValue
Kind multiplier (HP)BOSS_KIND_HP_MULT_MINI / BOSS_KIND_HP_MULT_BOSSmini = 4.0, boss = 8.0
Kind multiplier (damage)BOSS_KIND_DAMAGE_MULT_MINI / BOSS_KIND_DAMAGE_MULT_BOSSmini = 4.0, boss = 8.0
Depth ramplinear over the run level sequence1.00 × at level 1 → 1.50 × at the final boss level

Final per-body multipliers at spawn:

hpMult  = kindHpMult  * (1.00 + 0.50 * depthFrac)
dmgMult = kindDmgMult * (1.00 + 0.50 * depthFrac)

depthFrac = (currentLevel - 1) / (totalLevels - 1), clamped to [0, 1]. totalLevels is 5 in normal mode, 10 in challenge mode.

boss-scaling.ts defines a richer multi-dimensional scaling config (DEFAULT_BOSS_SCALING) that some systems consume directly (turret fire rate, move speed, limb sweep, spawner frequency, projectile speed, effect radius, enrage timer). New bosses inherit the defaults; per-boss overrides slot into the boss def via custom code paths if needed.

Default scaling dimensions (DEFAULT_BOSS_SCALING in data/boss-scaling.ts):

DimensionperLevelcapValuecapSteepnesscapType
hp2.00uncapped
damage2.15uncapped
turretFireRate1.253.00.30soft_cap
moveSpeed1.102.00.25soft_cap
limbSweepSpeed1.152.50.20soft_cap
spawnerFrequency1.203.00.25soft_cap
projectileSpeed1.122.0hard_cap
effectRadius1.102.50.20soft_cap
enrageTimer0.880.250.30soft_cap

BASE_ENRAGE_TIMER_SEC = 120 is the level-1 baseline enrage timer; per-boss defs can override.

Step 7: Rewards

BossDef.reward is a lump payout granted on win:

FieldTypeMeaning
xpnumberLump XP granted by the boss_kill signal.
currencynumberLump currency.
extraRewardPicksnumberAdditional level-up pick rolls offered after the encounter.

Reference values from shipped bosses:

BossxpcurrencyextraRewardPicks
Awakened Mech12001202
Digbot Swarm10001002
Reactor Core9501001
Hive Queen900901

BossDef.rewardCascadeTypes (optional) tunes the Supply Pod cascade ring spawned at the death point. Five prop typeIds form a ring around the kill anchor. If omitted, the standard SUPPLY_POD_CASCADE_TYPES default is used (2 × Scrap + 1 × Mineral + 1 × Comet + 1 × Magnetar). kind: 'boss' encounters spawn two cascades with an ~80 px jitter; kind: 'mini' encounters spawn one.

Shipped per-boss cascade compositions:

BossCascade ring
Awakened Mechdrone_wreck, drone_wreck, mineral_vein, magnetar_pulse, scrap_pile
Digbot Swarm(default)
Reactor Corecomet_fragment, comet_fragment, mineral_vein, magnetar_pulse, scrap_pile
Hive Queenscrap_pile, scrap_pile, scrap_pile, mineral_vein, magnetar_pulse

Step 8: Arena

BossDef.arena describes the fight room:

FieldTypeMeaning
shape'circle' | 'rect'Circular or rectangular arena.
sizenumber | { w, h }Circle: radius. Rect: width × height.
closingDurationnumber (seconds)How long the arena takes to close around the player at encounter start.
terrainTerrainPatternId (optional)Destructible terrain pattern applied at encounter start. Defaults to 'open'.

Note: arena.size here is the closing-circle size — the dynamic shrinking room used by the legacy bossRoom path. Sealed-arena boss levels (_levelKind === 'mini_boss' or 'boss') use a fixed square room driven by SEALED_ARENA_HALF_SIZE = 1400 (footprint 2800 × 2800 world units) and SEALED_ARENA_WALL_THICKNESS = 2000 from data/level-progression.ts; the boss’s own arena block is still honored for ability geometry (cardinal / ring / edge points are derived from these values via BossArena helpers in engine/boss/arena.ts).

Pillar / hazard patterns are layered in via terrain (e.g. pillar_ring for Hive Queen, corridor for Reactor Core, hazard_pads for Awakened Mech). They are applied by applyTerrainPattern(def.arena.terrain, arena, world) in spawnBoss.

Reference arenas:

BossShapeSizeClosingTerrain
Awakened Mechcircler = 3808 shazard_pads
Digbot Swarmcircler = 4508 sopen
Reactor Corerect800 × 4008 scorridor
Hive Queencircler = 4008 spillar_ring

BossDef.backgroundLoop (optional) attaches a pre-baked H.264 loop rendered behind the arena. Spec target: 1280 × 720, 5–10 s seamless loop, ~5–15 MB, audio stripped, hosted under public/boss-bg/. No currently shipped boss sets this.

Step 9: Custom abilities or AI

If an existing pattern or AI behavior does not fit, the work moves out of data/ and into the engine.

Custom caseWhere it goes
New ability patternAdd a new value to PatternId in engine/abilities/index.ts. Implement fire<Name>, optional tick<Name> (for sustained patterns), and optional execute<Name> (for telegraphed payloads) in engine/abilities/patterns.ts. Wire the switch cases in firePattern and tickSustained. Patterns crash on missing required params (loud, not silent) and call the VFX kit for telegraph + impact visuals.
New AI behaviorRegister a new behavior string in engine/enemies/behaviors.ts and extend mapAiIdToBehavior in engine/boss/encounter.ts so the new aiId resolves. Unknown aiIds crash at spawn — there is no silent fallback.
New affixAdd the affix def under data/affixes/, implement runtime in engine/affixes/runtime.ts. The respawn_as and shielded_respawn affixes are the reference shapes for body-swapping and anchor-respawn flows.
Lifecycle hooksBossDef exposes setupVfx, onBodyDeath, onDeath, onUpdate, onPhaseChanged. All are pure side-effect (no return value); they mutate the world and the shared VFX kit.

The shared VFX kit instance is returned by getSharedBossVfxKit() so out-of-module hooks (such as the respawn_as runtime) can re-bind layers without creating a second kit.

Step 10: Validate

Verification checklist after wiring a new boss:

CheckWhat to confirm
RegistryBOSS_DEFS[yourId] resolves at module load. pickRandomBossId(kind) returns your id for the matching pool.
SpawnspawnBoss(yourId, game, world, kind) materializes the roster at the expected positions and stamps game._activeBossDefId.
HP barAnchor body’s isBoss is true; bar shows the boss displayName; total bar pool sums per-body hp * hpMult.
ScalingPer-body hpMax = entry.hp * kindMult * depthMult matches the level you’re testing at. Damage scales via enemy.damageMult.
Phase transitionsCrossing each thresholds[i] swaps to nextTypes[i] (sprite changes), replaces abilities with nextAbilityIds[i], and re-applies nextAffixIds[i]. onPhaseChanged fires after the swap.
AbilitiesEach ability fires on its declared cooldown; telegraph durations match params; sustained patterns hold the cooldown gate until sustainMs expires.
Spawn profilePressure adds appear at the configured triggers and positions. phase-keyed waves fire only when the anchor crosses into the matching phase index.
RewardsOn win the boss_kill signal carries the def’s xp, currency, and id. The supply pod cascade ring spawns at the kill anchor with the configured prop ids. extraRewardPicks extra level-up picks are offered.
TeardownAll sharing/anchor bodies cull on win/loss; ability bullets clear on win/level-exit (persist on player death); terrain pattern is cleared; clearBossVfxLayers() drops VFX; game.bossArena, game.bossSpawnProfile, game.bossEncounterTime, game._activeBossDefId all reset.
No console errorsUnknown aiId, missing ability id, missing affix id, missing enemy type id, or missing pattern params all crash loud — none should appear in a passing test.