Ship Respawn (Revive-Accept Flow)

What the ship actually looks like the frame after the player taps RESPAWN. The revive prompt is the decision; this page is the state transition. “Respawn” in Starship Survivors means specifically the in-run revive-accept restoration — it is not a fresh spawn. The ship returns to play at its current world position with a partial top-up, a short invulnerability window, a phoenix VFX pulse, and a sonar shockwave that wipes the immediate threat. The flow is owned by bridge.revive() in src/starship-survivors/engine/bridge.ts. Constants live in src/starship-survivors/data/economy.ts under the DEATH_DEFIANCE_* prefix.

For the prompt timing, gem pricing, and decline path, see revive-system.

State write — what the ship looks like on the next frame

bridge.revive() is gated by _revivePromptActive && game.phase === 'dead'. The body is a fixed sequence of mutations against the live ship object:

FieldValueSource
ship.alivetruere-enters the live entity set
ship.hpMath.ceil(ship.hpMax * DEATH_DEFIANCE_HP_RESTORE)50% of max HP, rounded up
ship.shieldship.shieldMaxfull shield, not partial
ship.invulnerabletrueiframe flag for collision and bullet checks
ship.invulnTimerDEATH_DEFIANCE_INVULN_SECONDS (3.0 s)counts down per-frame; flag clears at zero
ship.x, ship.yunchangedthe ship comes back exactly where it died
ship.angleunchangedfacing is preserved
ship.vx, ship.vyunchangedvelocity is preserved (usually near zero from the death frame)

Position-preserved is load-bearing. The player is paying gems specifically to keep the run going from where they were — not to re-enter at a hub or arena origin. The shockwave (below) is what makes the unchanged position survivable.

The bridge also flips control-flow state alongside the ship mutations:

  • _deathDefianceUses++ — drives the next prompt’s cost via deathDefianceCost(useNum + 1).
  • _revivePromptActive = false, _deathCinematicPlaying = false — closes the modal-side state.
  • game.revivePrompt = false, game.reviveAvailable = false — clears the public flags the React layer reads.
  • game.tracking.deathDefianceUsed = true — run-end telemetry marker.
  • game.phase = 'playing' + Sig.fire('phase_change', 0, 0, 0, 0, 'playing') + callbacks.onPhaseChange('playing') — resumes the main loop.

Phoenix VFX — three layers in one frame

All three particle calls fire in revive() immediately after the phase flip, anchored to ship.x, ship.y:

LayerCallEffect
Gold spark burstParticles.burst(ship.x, ship.y, 40, 'spark', { r: 255, g: 220, b: 80 }, 150)40 outward sparks, gold tint, 150 unit radius
Yellow rising sparksfor (let pi = 0; pi < 20; pi++) Particles.add(...)20 sparks with strong upward drift (vy = -60 to -140), randomized lifetime 0.5–1.0 s — the “phoenix rising” feel
White-hot flashParticles.burst(ship.x, ship.y, 8, 'spark', { r: 255, g: 255, b: 255 }, 40)8 white sparks at ship center, tight 40 unit radius

Plus Juice.fire('revive') for hit-stop and shake.

Sonar shockwave — clears the field

Two expanding rings via SonarRings.shockwave(...):

RingCallColor
OuterSonarRings.shockwave(ship.x, ship.y, ship.radius, 800, '#ff6600', 1.0, 5)Orange, 800 unit max radius
InnerSonarRings.shockwave(ship.x, ship.y, ship.radius * 0.6, 650, '#ff9944', 0.7, 3)Lighter orange, 650 unit max radius

The visual ring is paired with a damage broadcast. The revive iterates world.enemies and applies damageEnemy(re, 999999, game, ship, world) to every non-boss enemy, with two carve-outs:

  • re.isBoss || re.sharesHealthWithBossskipped. Boss bodies and shared-health minions are deliberately spared. The boss damage pipeline owns their teardown and a 999999-damage broadcast would corrupt it.
  • re._chargerPhase — temporarily zeroed before the damage call and restored after, so charger immunity doesn’t block the wipe.

If somehow re.alive is still true after the damage call, the loop force-sets re.hp = 0 and re.alive = false as a backstop.

After the enemy sweep, world.enemyBullets.length = 0 — every hostile projectile in flight is dropped on the floor.

The shockwave is intentional, not cosmetic: the screen state that killed the player is still hostile, and the player came back at the same coordinates. Wiping the field gives a clean lane to recover.

Why this is not “initial spawn”

Initial run spawn uses a “birth phase” — the ship is placed at the arena/hub origin with a separate intro VFX, full HP, no shockwave, and the standard spawn-immunity window. Respawn-via-revive is a different code path:

AspectInitial spawn (birth phase)Revive-accept respawn
PositionArena/hub originUnchanged (where the ship died)
HPFull (hpMax)50% (ceil(hpMax * 0.5))
ShieldFull (shieldMax)Full (shieldMax)
InvulnSpawn-immunity windowDEATH_DEFIANCE_INVULN_SECONDS (3.0 s)
VFXBirth introPhoenix burst + rising sparks + white flash
Field clearn/a (nothing to clear)Sonar shockwave kills non-boss enemies + clears enemy bullets
Countern/a_deathDefianceUses++ increments per-run revive count
Phase transitionloadingplayingdeadplaying

Same player object, very different state-machine entrypoint. If you’re touching one path do not assume the other behaves the same way.

  • revive-system — full revive lifecycle: death detection, cinematic gate, prompt, accept/decline/timeout exits, pricing
  • death-defiance-timing — non-linear prompt timer math
  • spawn-immunity-window — initial-spawn invuln semantics (contrast point)
  • engine/bridge.tsrevive() body, declineRevive(), dead-phase tick
  • data/economy.tsDEATH_DEFIANCE_HP_RESTORE (0.5), DEATH_DEFIANCE_INVULN_SECONDS (3.0)