Achievement Tracker
Per-run counters that the engine accumulates while a mission is live, plus the stable lifetime-progress key list those counters feed into on mission end. Drives challenges, prologue beats, and the hub’s achievement display.
Two layers
The tracker has two distinct surfaces — the in-run mutable bag and the lifetime persistent counters.
Layer 1 — game.tracking (per-run, in-memory)
A single object on the GameState (see engine/core/types.ts → GameTracking, initialized in engine/core/state.ts) that the engine mutates during a run. Fields are typed and pre-allocated at run start — no map churn. The engine writes to them from engine/bridge.ts as events fire:
damageTaken— accumulated on hull hittotalHeal,totalShieldRegen— accumulated on regen tickslowestHpPercent— running minimum ofhp / hpMaxdeathDefianceUsed,reviveTokenUsed— boolean flags set when the token is consumedeventsCompleted,eventsAttempted— bumped on event start/finishweaponsFound,upgradesChosen,artifactsCollected— string arrays pushed on acquireartifactBestTier— optionalRecord<artifactId, 0-4>of highest runtime tier seenabilityUses,maxDistance,detections,trapKills,trapKillPctgatesHit,gatesRequired,payloadHpRemainingPct,objectHpRemainingPct— per-event-type scratch fieldspoisVisited,subZoneEntered,enemyBulletsFired,enemyBulletsHit
This bag is the canonical per-run source of truth. It is wiped on every mission start.
Layer 2 — PROGRESS_KEYS (lifetime, server-persisted)
data/progress-keys.ts exports the canonical list of ~15 stable counter names that survive across runs. These are the keys the achievement system and Rookie Week prologue check against:
total_arcade_runs, total_kills, elite_kills, boss_kills,
events_completed, crates_destroyed, max_survival_seconds,
missions_deployed, missions_claimed, pulls_made, merges_done,
buildings_placed, ships_owned, max_run_level, weapon_chests_opened
ProgressKey is the union type derived from this list. Anything that consumes lifetime progress (prologue beats, challenge predicates, hub achievement tiles) addresses counters by these exact strings.
Mission-end flow
When a run ends, engine/bridge.ts fires onGameOver with the engine’s MissionResult. The metagame’s services/runProgressionService.ts → finalizeRun(result, planetIndex) builds the RPC payload and calls the Supabase finalize_run RPC. The payload carries the run summary the server needs to compute rewards and update lifetime counters:
ship_id, duration_s, kills, wave_reached, xp_earned,
result (survived|died), credits_earned, scrap_earned,
tier_reached, planet_id, weapons_at_end, events_completed
Server-side, finalize_run atomically:
- inserts
match_history - computes currency rewards (credits / scrap)
- updates
player_currencies+ inserts transactions - (post-migration 031) returns newly-completed challenges + updated planet kill/event totals
- (post-migration 033) returns updated planet XP
The response is the canonical wallet snapshot — runProgressionService replaces local wallet state from it, then updates the tier store, challenge store, and planet-progress store from the same response. Sync status is flipped syncing → synced (or error).
The lifetime PROGRESS_KEYS counters are kept in sync with these run-end payloads — the server is authoritative; clients display whatever the latest snapshot says.
Consumers
- Challenges — predicates compare
PROGRESS_KEYSdeltas (e.g.total_killsreaching N on planet X). - Prologue beats —
data/prologue-config.tsdeclares each beat’s completion as{ tracking: ProgressKey, target: number }. The four beats currently key offtotal_kills,max_run_level,events_completed, andpulls_made. - Hub achievement display — the metagame reads the same lifetime counters to paint progress tiles and unlock states.
Why two layers
In-run mutations need to be cheap and untyped-stringly (the engine ticks every frame). Lifetime persistence needs a stable, narrow, server-validated key set so RPC schemas and challenge predicates don’t break when engine internals change. The MissionResult is the contract between them — engine flattens game.tracking + game.stats into the result shape, then finalizeRun projects that into the RPC payload.
Files
src/starship-survivors/data/progress-keys.ts—PROGRESS_KEYSconst,ProgressKeytypesrc/starship-survivors/engine/core/types.ts—GameTrackinginterfacesrc/starship-survivors/engine/core/state.ts—trackinginitializationsrc/starship-survivors/engine/bridge.ts— per-rungame.tracking.*writessrc/starship-survivors/services/runProgressionService.ts—finalizeRunRPC + store updatessrc/starship-survivors/data/prologue-config.ts— exampleProgressKeyconsumer