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:
| Field | Value | Source |
|---|---|---|
ship.alive | true | re-enters the live entity set |
ship.hp | Math.ceil(ship.hpMax * DEATH_DEFIANCE_HP_RESTORE) | 50% of max HP, rounded up |
ship.shield | ship.shieldMax | full shield, not partial |
ship.invulnerable | true | iframe flag for collision and bullet checks |
ship.invulnTimer | DEATH_DEFIANCE_INVULN_SECONDS (3.0 s) | counts down per-frame; flag clears at zero |
ship.x, ship.y | unchanged | the ship comes back exactly where it died |
ship.angle | unchanged | facing is preserved |
ship.vx, ship.vy | unchanged | velocity 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 viadeathDefianceCost(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:
| Layer | Call | Effect |
|---|---|---|
| Gold spark burst | Particles.burst(ship.x, ship.y, 40, 'spark', { r: 255, g: 220, b: 80 }, 150) | 40 outward sparks, gold tint, 150 unit radius |
| Yellow rising sparks | for (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 flash | Particles.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(...):
| Ring | Call | Color |
|---|---|---|
| Outer | SonarRings.shockwave(ship.x, ship.y, ship.radius, 800, '#ff6600', 1.0, 5) | Orange, 800 unit max radius |
| Inner | SonarRings.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.sharesHealthWithBoss— skipped. 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:
| Aspect | Initial spawn (birth phase) | Revive-accept respawn |
|---|---|---|
| Position | Arena/hub origin | Unchanged (where the ship died) |
| HP | Full (hpMax) | 50% (ceil(hpMax * 0.5)) |
| Shield | Full (shieldMax) | Full (shieldMax) |
| Invuln | Spawn-immunity window | DEATH_DEFIANCE_INVULN_SECONDS (3.0 s) |
| VFX | Birth intro | Phoenix burst + rising sparks + white flash |
| Field clear | n/a (nothing to clear) | Sonar shockwave kills non-boss enemies + clears enemy bullets |
| Counter | n/a | _deathDefianceUses++ increments per-run revive count |
| Phase transition | loading → playing | dead → playing |
Same player object, very different state-machine entrypoint. If you’re touching one path do not assume the other behaves the same way.
Related
revive-system— full revive lifecycle: death detection, cinematic gate, prompt, accept/decline/timeout exits, pricingdeath-defiance-timing— non-linear prompt timer mathspawn-immunity-window— initial-spawn invuln semantics (contrast point)engine/bridge.ts—revive()body,declineRevive(), dead-phase tickdata/economy.ts—DEATH_DEFIANCE_HP_RESTORE(0.5),DEATH_DEFIANCE_INVULN_SECONDS(3.0)