PURPOSE

Persists completed match results to the server via the finalize_run Supabase RPC. Called after the engine bridge fires onGameOver. The server atomically inserts into match_history, calculates currency rewards, updates player_currencies, and inserts transactions. Returns a canonical finalized response (match_id, wallet snapshot, optional tier/challenge/planet records) that drives ResultsScreen display and downstream store updates.

OWNS

  • finalizeRun(result: MissionResult, planetIndex: number): Promise<FinalizeRunResponse> — single exported function.
  • FinalizeRunResponse interface — shape of the RPC return payload (match_id, wallet, rewards, optional tier_record, challenge_completions, planet_stats, planet_xp).
  • Client-side computation of credits_earned via computeArcadeCredits(result).
  • Sync-status lifecycle around the RPC call (syncingsynced | error).

READS FROM

  • useSessionStore.getState().runDef?.context.planetId — actual planet id (3, 12, 21), defaults to 0 if missing.
  • MissionResult fields: identity.shipId, performance.timeElapsedSeconds, combat.totalKills, progression.levelReached, progression.tierReached, progression.weaponsAtEnd, progression.eventsCompleted, outcome.survived.
  • computeArcadeCredits from ../data/economy.
  • invokeRpc from @metagame/services/supabase.

PUSHES TO

  • useWalletStore.replaceFromSnapshot({ gems, credits }) — canonical wallet update from server response.
  • useTierStore.updateFromRunResult(planet_id, highest_tier) — when tier_record present.
  • useChallengeStore.addCompletions(challenge_completions) — when array non-empty.
  • useChallengeStore.updatePlanetStats(planet_id, total_kills, total_events) — when planet_stats present.
  • usePlanetProgressStore.setXp(planet_id, xp) — when planet_xp present; planet_id narrowed to 3 | 12 | 21.
  • usePlayerStore.setSyncStatus('syncing' | 'synced' | 'error') — bracketing the RPC.
  • Supabase finalize_run RPC — payload with run identity, performance, combat, progression, planet, weapons_at_end, events_completed.

DOES NOT

  • Compute or send xp_earned (hardcoded 0) or scrap_earned (hardcoded 0).
  • Use planetIndex parameter for the RPC payload — planet_id is read from session store instead.
  • Mutate MissionResult or any local game state.
  • Render or display anything — ResultsScreen consumes the return value separately.
  • Retry on RPC failure — error propagates; sync_status flips to error.
  • Author currency rewards server-side — credits are computed client-side (noted as moving server-side for real-money launch).
  • Persist before missionResult is set in sessionStore (caller contract).

Signals

  • syncing / synced / error sync-status broadcast through usePlayerStore.
  • Thrown error on RPC failure — caller responsible for handling.
  • Wallet snapshot replacement triggers wallet store subscribers.
  • Tier, challenge, planet-stats, and planet-xp store updates fire individually only when their respective response fields are present (defensive against pre-migration responses).

Entry points

  • Called after the engine bridge onGameOver fires, post missionResult being set in sessionStore.
  • Caller must await completion before allowing user interaction with ResultsScreen rewards.
  • Return value flows into ResultsScreen for canonical display.

Pattern notes

  • Single async function module; no class, no internal state.
  • All state mutations go through Zustand getState() calls — no React hooks here (service layer).
  • Defensive optional-chaining on response fields tied to migration phases (031 for challenges/planet_stats, 033 for planet_xp).
  • Wallet is replaced from snapshot rather than incremented locally — server is canonical.
  • xp_earned and scrap_earned are wired but zero-valued, reserved for future expansion.
  • tier_reached is forwarded from MissionResult.progression, distinct from wave_reached/levelReached.
  • result field is a string enum ('survived' | 'died') derived from outcome.survived.
  • Planet id sourced from runDef.context.planetId (canonical 3/12/21), explicitly distinguished from the planetIndex (0-2 chapter index) parameter, which is accepted but not forwarded to the RPC.
  • Sync-status try/finally pattern is split: error is set in the catch, synced only after all store updates succeed.