Revive System (Death Defiance)
In-run resurrection mechanic. On player death the bridge plays a “YOU DIED” cinematic, then surfaces a modal asking the player to spend gems for a second chance. Up to three revives per run, with an escalating gem cost. Failure to accept ends the run and routes to mission results. Constants live in src/starship-survivors/data/economy.ts under the DEATH_DEFIANCE_* prefix; the UI is src/starship-survivors/components/RevivePrompt.tsx; flow control lives in src/starship-survivors/engine/bridge.ts.
Pricing and limits
| Constant | Value | Meaning |
|---|---|---|
DEATH_DEFIANCE_BASE_COST | 20 | Base gem cost (first use of the run) |
DEATH_DEFIANCE_COST_ESCALATION | 1 | Cost is BASE_COST × useNumber, so 1st = 20, 2nd = 40, 3rd = 60 |
DEATH_DEFIANCE_MAX_USES | 3 | Hard cap per run; after this the prompt no longer appears |
DEATH_DEFIANCE_FREE_DAILY | 1 | Free daily revive token (separate from gem-purchased uses) |
DEATH_DEFIANCE_HP_RESTORE | 0.5 | Fraction of hpMax restored on revive |
DEATH_DEFIANCE_INVULN_SECONDS | 3.0 | Post-revive iframe window |
deathDefianceCost(useNumber) is the canonical pricing function — UI and bridge both call it instead of computing the multiplication inline.
Lifecycle
The bridge state machine drives the entire flow. Phase transitions from playing → dead → either playing (revive accepted) or results (declined / timed out).
- Death detected —
ship.aliveflips false in the per-frame check. The bridge firesSampler.event('player_death', ...), stops music, callsonBossEncounterEndif a boss arena was active, and setsgame.phase = 'dead'. - Cinematic gate — if
_deathDefianceUses < DEATH_DEFIANCE_MAX_USES, the bridge enters_deathCinematicPlaying = truewith_deathCinematicTimer = DEATH_DEFIANCE_CINEMATIC_DURATION(3.0 s). The canvas draws the “YOU DIED” vignette + text during this window. If max uses are already exhausted, the cinematic is skipped —game.deathTimeris set to 1.0 and the run hard-cuts to results. - Prompt show — when the cinematic timer hits zero, the bridge sets
_revivePromptActive = true,_revivePromptTimer = DEATH_DEFIANCE_TOTAL_REAL_DURATION(~7.75 s), andgame.revivePrompt = true. It firescallbacks.onRevivePrompt(deathDefianceCost(useNum), useNum)so the React layer mountsRevivePrompt. - Player decision — three exits:
- Accept —
bridge.revive()is called (afteruseWalletStore.spendGems(cost)succeeds in the component). Restores ship, fires phoenix VFX, clears enemies, resumes play. - Decline —
bridge.declineRevive()is called. Setsgame.deathTimer = 0.01for an instant hard cut to results. - Timeout —
_revivePromptTimerreaches zero with no input. Bridge clears the prompt flags and callsonReviveDismissed; theRevivePromptcomponent handles the explosion/fade transition then calls itsonDeclineprop.
- Accept —
- Results phase — on decline / timeout / final death,
game.phaseadvances toresultsoncegame.deathTimer ≤ 0. Telemetry fires,MissionResultis populated, andcallbacks.onGameOver(missionResult)ships the run summary to the screen layer.
While the prompt is up, gameplay is frozen — _revivePromptActive short-circuits the normal death-timer countdown so enemies don’t keep moving and the death cinematic doesn’t keep elapsing in any meaningful way.
Accept flow — what bridge.revive() does
The revive does more than top up HP. It re-asserts a safe playable state so the player isn’t immediately re-killed by whatever just killed them.
| Step | Effect |
|---|---|
_deathDefianceUses++ | Increments the per-run counter. Drives the next prompt’s cost via deathDefianceCost(useNum + 1). |
_revivePromptActive = false / _deathCinematicPlaying = false | Closes the modal state. |
game.revivePrompt = false / game.reviveAvailable = false | Clears the public flags the React layer reads. |
game.tracking.deathDefianceUsed = true | Run-end telemetry marker. |
ship.alive = true | Re-enters the live entity set. |
ship.hp = ceil(hpMax × DEATH_DEFIANCE_HP_RESTORE) | 50% HP back. |
ship.shield = ship.shieldMax | Full shield restore. |
ship.invulnerable = true, ship.invulnTimer = DEATH_DEFIANCE_INVULN_SECONDS | 3 s of iframes. |
game.phase = 'playing' + Sig.fire('phase_change', ..., 'playing') | Resumes the loop. |
| Phoenix VFX | Gold spark burst, 20 upward-drifting yellow sparks, white center flash. |
| Sonar shockwave | Two expanding rings (orange #ff6600 + lighter #ff9944) — kills every non-boss enemy in the world and clears world.enemyBullets. |
Juice.fire('revive') | Hit-stop / shake feedback. |
The shockwave is intentional: the player just paid gems to come back, and the screen state that killed them is still hostile. Wiping the field gives a clean lane to recover. Boss bodies and shared-health minions are deliberately spared — the boss damage pipeline owns their teardown and a 999999-damage broadcast would corrupt it.
Decline flow — what bridge.declineRevive() does
Decline is a hard cut. No fade, no death-timer delay.
_revivePromptActive = false,_deathCinematicPlaying = false_postDeclineDeath = true— suppresses the “YOU DIED” text re-showing during the final result transition (theRevivePromptcomponent is already playing its own fade-to-black, so the canvas vignette would double-draw).game.revivePrompt = false,game.reviveAvailable = falsegame.deathTimer = 0.01— next frame the dead-phase tick firesgame.phase = 'results'.Juice.fire('player_death')+callbacks.onReviveDismissed().
The component-side transition (white flash → fade to black) is what the player actually sees during this hard cut; the bridge just gets out of the way fast.
UI side — RevivePrompt.tsx
The React component receives gemCost, useNumber, onAccept, and onDecline props. It owns three things the bridge does not:
- Cheater timer — non-linear bar drain. See
death-defiance-timingfor the full math. The bridge’s_revivePromptTimermatchesDEATH_DEFIANCE_TOTAL_REAL_DURATIONso the auto-dismiss fires exactly when the visual bar ends. - Affordability gate — reads
useWalletStore.gems. Ifgems < gemCost, the RESPAWN button disables and aNot enough gems (X/Y)line shows below. Decline still works. - Death transition — on decline or timeout, plays a 0.4 s white flash + 0.8 s fade to black before calling
onDecline(). Bridge’sdeclineReviveruns immediately when the user taps decline; the visual transition is a parallel animation the bridge doesn’t gate on.
Audio is hand-rolled with AudioContext: an 80 Hz sine gong on timeout / decline, a stacked A4/E5/A5/E6 chime on respawn.
State surface
game (engine core state) exposes three flags the React layer subscribes to:
| Field | Meaning |
|---|---|
game.revivePrompt | Boolean — RevivePrompt mounts when true. |
game.reviveTimer | Reserved counter for the prompt timer (mirror of _revivePromptTimer). |
game.reviveAvailable | Set during cinematic; true means the player has revives remaining. |
game.tracking.deathDefianceUsed is a per-run telemetry flag set true the first time revive() runs.
The bridge keeps the source-of-truth counter in module-scope: _deathDefianceUses, _revivePromptActive, _revivePromptTimer, _deathCinematicPlaying, _deathCinematicTimer, _postDeclineDeath. The game.* mirrors exist purely so the React layer doesn’t reach into bridge internals.
Related
death-defiance-timing— the cheater-timer + linger math behind the prompt’s countdowndata/economy.ts— allDEATH_DEFIANCE_*constants anddeathDefianceCost()components/RevivePrompt.tsx— modal UI, audio, transitionengine/bridge.ts—revive(),declineRevive(), dead-phase tick, callback wiringengine/core/state.ts—game.revivePrompt,game.reviveTimer,game.reviveAvailable