data/run-history
What flows in
MissionResultobject (from./mission-result) — passed tosaveRunToHistory()after a run ends.nodeId: string— per-planet key used for personal-best lookups and writes.- Raw numeric values for tier / kills / events / level — fed to
save*PB()strictly-improving writes.
What flows out
RunHistorySummary[]— newest-first list returned bygetRunHistory(), capped at 20 entries.RunHistorySummary | null— per-field averages fromgetRunAverages(history);nullif no history.{ bestScore, bestKills, bestTime, bestTier, bestDamage }— extremes fromgetPersonalBests(history).numberspawn multiplier fromgetNewAccountSpawnMult()— 0.5 / 0.7 / 0.9 / 1.0 based on prior-run count.- Per-planet PB numbers from
getTierPB()/getKillsPB()/getEventsPB()/getLevelPB(). booleanfromsave*PB(nodeId, v)—trueonly when the new value strictly beat the prior PB and the write succeeded.
Key symbols
RunHistorySummaryinterface — 16-field compact snapshot of a run (identity, performance, outcome, combat, progression, economy, exploration).HISTORY_KEY = 'ss_run_history'— localStorage key for the FIFO buffer.MAX_RUNS = 20— hard cap on stored runs; FIFO drops oldest.TIER_PB_KEY_PREFIX = 'ss_tier_pb_'— per-planet best tier reached.KILLS_PB_KEY_PREFIX = 'ss_kills_pb_'— per-planet best total kills.EVENTS_PB_KEY_PREFIX = 'ss_events_pb_'— per-planet best event count.LEVEL_PB_KEY_PREFIX = 'ss_level_pb_'— per-planet best level reached.readPB(prefix, nodeId)— parseslocalStorageint, returns0on missing / non-finite / non-positive / storage error.writePB(prefix, nodeId, value)— strictly-improving: rejects non-finite,<= 0, or<= prev; swallows storage failure.buildRunSummary(result)— extracts 16 fields fromMissionResultand stampsplayedAtwithnew Date().toISOString().saveRunToHistory(result)—unshifts new summary, truncates toMAX_RUNS,JSON.stringify→localStorage.setItem.getRunHistory()—JSON.parsefromlocalStorage; returns[]on missing / parse error.getNewAccountSpawnMult()— new-account difficulty dampener; readsgetRunHistory().lengthand maps to spawn-rate multiplier.getRunAverages(history)— sums each numeric field, divides byn,Math.rounds; identity / outcome / rewardTier fields blanked.getPersonalBests(history)—Math.maxoverscore,totalKills,timeElapsedSeconds,tierReached,damageDealt.saveTierPB/saveKillsPB/saveEventsPB/saveLevelPB— public wrappers aroundwritePB.getTierPB/getKillsPB/getEventsPB/getLevelPB— public wrappers aroundreadPB.
Where it’s used
- Post-run stats / reveal screens — read history for comparison and averages.
RevealScreenfour progress bars — driven by per-planet tier / kills / events / level PBs.LootBagdrop-count rule — uses tier PB only (per code comment).- Spawn-rate logic — calls
getNewAccountSpawnMult()to soften the first ~10 sessions. - Mission completion flow — calls
saveRunToHistory(result)with the finalMissionResult.
Gotchas
- This is the one data file that touches
localStoragedirectly. All other persistence in the game runs through Supabase or in-memory stores. Anything that needs cross-device sync does not belong here. - Browser-only. Any caller that imports this module must run in a browser context — there’s no
typeof windowguard, but everylocalStorageaccess is wrapped intry/catchthat swallows failure. - Silent failures everywhere.
localStoragequota-exceeded, parse errors, missing keys — all swallowed with empty-array / zero defaults. No telemetry, no Sentry breadcrumb, no user-visible signal. - FIFO is
unshift+length =, not a ring buffer. Newest run is always index 0; oldest is dropped by re-assigninghistory.length = MAX_RUNS. - Strictly-improving PB writes.
writePBreturnsfalseon stale value — callers that need to know “did I just set a new PB?” should check the return value. Equal-to-prev is treated as stale (not a new PB). readPBaccepts only positive integers. A stored0, negative,NaN, orInfinityreads back as0.parseIntwith radix 10 is used — trailing non-numeric characters are silently truncated.getNewAccountSpawnMultcaps at prior-run count of 10+, but the history buffer caps at 20. The comment claims “10 or more reliably means normal rate even on very long accounts” — this is correct only becauseMAX_RUNS = 20 >= 10. IfMAX_RUNSever drops below 10, the dampener will re-engage for long-tenured players.- Averages blank out non-numeric fields.
getRunAveragesreturns aRunHistorySummaryshape withplayedAt: '',nodeId: '',shipId: '',rewardTier: '',survived: false. Consumers must treat the result as numeric-only. - No migration / versioning. If
RunHistorySummaryever adds or renames a field, old stored runs will deserialize withundefinedfor the new field. No schema version is written alongside the data. - No multi-tab coordination. Two tabs writing concurrently will last-writer-wins on
JSON.stringify(history); nostorageevent listener. buildRunSummarystampsplayedAtat save time, not run-end time. If the call is delayed, the timestamp drifts from actual end-of-run.
EXTRACT-CANDIDATE
This file mixes three concerns:
- Run history FIFO buffer (
ss_run_history,MAX_RUNS,saveRunToHistory,getRunHistory,getRunAverages,getPersonalBests). - Per-planet personal bests (
ss_tier_pb_*,ss_kills_pb_*,ss_events_pb_*,ss_level_pb_*and theirreadPB/writePBhelpers). - New-account difficulty dampener (
getNewAccountSpawnMult) — which only reads run-count, not history content.
If localStorage is ever migrated to Supabase / IndexedDB / encrypted bundle, the boundary cuts cleanly along these three axes. A future split could be:
data/run-history.ts— FIFO summaries only.data/planet-pbs.ts— strictly-improving per-planet bests with a sharedreadPB/writePBcore.data/new-account-ramp.ts— spawn dampener (one function, one number).
This is also the natural seam for adding storage-failure telemetry: today every try/catch swallows silently, but a single shared safeRead / safeWrite wrapper could emit a Sentry breadcrumb on quota-exceeded.