PURPOSE

React overlay component shown after the “YOU DIED” cinematic. Offers the player a one-tap revive in exchange for a gem cost, gated by a stretched countdown timer with escalating UI panic. On accept, spends gems via the wallet store and calls onAccept; on decline / timeout, plays a white-flash → fade-to-black transition before calling onDecline. Mounted inside the game HUD layer; visibility driven by game.revivePrompt state from the bridge.

OWNS

  • Component-local React state:
    • timeLeft — display-time seconds remaining (drives bar + button label).
    • lingering — bar has hit 0% and is holding at ~1% for tension.
    • transitioning — explosion/fade overlay is playing.
    • transitionElapsed — seconds elapsed inside the transition.
    • sparkSeed — re-randomization tick counter for spark particle positions.
  • Refs:
    • startTime — wall-clock anchor (Date.now()) for the countdown.
    • declineFiredRef — guard against double-firing onDecline.
    • lingerStartRef — wall-clock anchor for the linger phase.
    • gongFiredRef — guard so the timeout/decline gong only plays once.
  • Two private intervals:
    • 50ms countdown tick (bar/shake/sparks/linger detection).
    • 30ms transition tick (explosion → fade interpolation).
  • Two private Web Audio one-shots: playGong (decline/timeout) and playRespawnChime (accept).
  • Pure helpers cheaterTimeRemaining(realElapsed) (real→display time map), plus module-level constants BAR_REAL_DURATION, TOTAL_REAL_DURATION, EXPLOSION_DURATION, FADE_TO_BLACK_DURATION, TRANSITION_DURATION, SPARK_COUNT, LINGER_DURATION.

READS FROM

  • useWalletStoregems (current balance) and spendGems (debit function).
  • @starship-survivors/data/economyDEATH_DEFIANCE_PROMPT_DURATION, DEATH_DEFIANCE_CHEAT_THRESHOLD, DEATH_DEFIANCE_CHEAT_STRETCH, DEATH_DEFIANCE_LINGER_DURATION. Cost / threshold / stretch numbers are not duplicated here.
  • Props: gemCost, useNumber (1..3), onAccept, onDecline.
  • Date.now() for timer math; Math.random() for shake jitter; window.AudioContext (with webkitAudioContext fallback) for SFX.

PUSHES TO

  • walletStore.spendGems(gemCost) on accept — wallet store handles the debit and validates affordability.
  • Parent-provided onAccept() — caller wires this to bridge.revive().
  • Parent-provided onDecline() — caller wires this to bridge.declineRevive().
  • No direct bridge / engine calls and no telemetry from this component.

DOES NOT

  • Does not read or write game.revivePrompt directly — visibility is the parent’s responsibility.
  • Does not decide cost, use-cap, or affordability rules — those live in the caller and the economy table.
  • Does not call bridge.revive / bridge.declineRevive itself — only the parent-supplied callbacks.
  • Does not pause the game loop, dim audio, or freeze the engine — pure overlay.
  • Does not persist anything across mounts; unmount discards timer state.
  • Does not respect prefers-reduced-motion — shake and sparks run unconditionally.
  • Does not handle keyboard or gamepad input — pointer-only via two <button> elements.
  • Does not block touch outside the card; full-screen touchAction: 'none' overlay swallows other gestures while mounted.
  • Does not localize the strings LAST STAND, RESPAWN — {n} gems, No thanks ({n}s), Use {n} of 3 — Continue the fight?, Not enough gems.

Signals

  • Cheater timer. cheaterTimeRemaining maps real elapsed seconds to a non-linear display countdown: 1:1 mapping for the early window, then the final DEATH_DEFIANCE_CHEAT_THRESHOLD fraction is stretched by DEATH_DEFIANCE_CHEAT_STRETCH so the last seconds feel longer than they read.
  • Linger phase. When display time falls to ≤ 0.15s the bar pins at 1% and the timer label sticks at 0.1; after LINGER_DURATION seconds the auto-decline transition fires. BAR_REAL_DURATION + DEATH_DEFIANCE_LINGER_DURATION is the absolute wall-clock cap.
  • Urgency factor (0..1). Derived from 1 - timeLeft / DEATH_DEFIANCE_PROMPT_DURATION, clamped to 1 during linger. Drives:
    • Card border / glow color (orange → red past 0.7).
    • Header color and text-shadow intensity.
    • Box-shadow blur radius.
    • Bar fill gradient (orange → orange-red at >0.5, red at >0.7).
    • Spark visibility (active when urgency > 0.7).
    • Screen shake: quadratic ramp on (urgency - 0.5) * 2, peaking at 6px translate jitter on the card.
  • Spark particles. SPARK_COUNT deterministic-ish offsets seeded by sparkSeed * 7 + i * 13; re-randomized every 50ms tick. Two colors (#ffaa33 every third spark, else #ff3344).
  • Timeout/decline gong. playGong — 80 Hz sine, gain 0.18 → 0.001 exponential decay over 2s. Played once via gongFiredRef.
  • Accept chime. playRespawnChime — four staggered sine partials (A4/E5/A5/E6), 60ms stagger, ~0.6s envelope each.
  • Transition overlay. While transitioning, the card is unmounted and replaced by a fixed full-screen <div>: ramps to opaque white over EXPLOSION_DURATION * 0.3 of the explosion window, holds white for the remainder, then linearly fades white→black over FADE_TO_BLACK_DURATION before invoking onDecline.

Entry points

  • RevivePrompt({ gemCost, useNumber, onAccept, onDecline }) — default named export.
  • Internal handlers:
    • handleRevive — guards on !canAfford || transitioning, attempts spendGems, plays chime, calls onAccept.
    • handleDecline — guards on transitioning, delegates to startTransition.
    • startTransition — plays gong once, flips transitioning, resets transitionElapsed.

Pattern notes

  • Wall-clock timers (Date.now()) not engine ticks — overlay is decoupled from any paused game loop. Single source of truth is startTime.current; timeLeft state is derived per tick.
  • Two effects, gated by transitioning: countdown effect early-returns once transition starts, transition effect early-returns until it does. The cleanup clearInterval runs on every re-render of the effect; deps [transitioning, lingering] cause the countdown effect to re-mount when linger flips — the new interval re-reads startTime.current, so timing is preserved.
  • Double-fire protection via two separate refs (declineFiredRef, gongFiredRef) — both are required because handleDecline and the natural timeout both route through startTransition.
  • All visuals are inline styles; no CSS module, no Tailwind class, no styled-components. Fonts: Cal Sans for headings/CTA, Space Grotesk for body. V32 palette: #ff6600 (orange), #ff3344 (red), #0a0a14 (card bg), slate greys for disabled / muted.
  • Spark animation is render-driven, not CSS-keyframe-driven — sparkSeed bump on each 50ms tick causes React to remount sparks with new keys and new transform offsets. The transition: transform 0.15s ease-out smooths the per-tick jump.
  • Audio uses a fresh AudioContext per call, closed via setTimeout after the envelope finishes. Wrapped in try/catch — silent failure on browsers without Web Audio. No interaction with the engine’s audio system.
  • No onUnmount cancel path for in-flight AudioContexts — if the prompt unmounts during a gong, the tone continues until its own setTimeout closes the context.

EXTRACT-CANDIDATE

  • playGong / playRespawnChime — bespoke Web Audio one-shots that bypass the engine audio system. If more overlays need ad-hoc SFX, hoist into a shared components/_audio.ts (or fold into the engine audio service) so envelope shapes and the AudioContext lifecycle live in one place.
  • cheaterTimeRemaining + the BAR_REAL_DURATION / TOTAL_REAL_DURATION derivation — a generic “stretched countdown” helper. If any other UI (boss timer, shop offer, missile lock) wants the same non-linear pacing, extract to lib/cheaterTimer.ts keyed on (total, threshold, stretch).
  • Urgency-driven shake/glow/color-ramp block — same primitive likely wanted by any “timer-of-doom” overlay. Candidate for a useUrgencyVisuals(progress) hook returning { shakeX, shakeY, glowColor, borderColor, showSparks }.
  • Inline overlay style (position: fixed; inset: 0; zIndex: 2000; touchAction: 'none') is duplicated between the live prompt and the transition overlay — fine here, but the same z-index / touch-blocking pattern recurs across HUD overlays and could move to a shared <FullScreenOverlay> wrapper.