Persistence
Four independent localStorage stores, and the save that strips transient state before snapshotting.
All persistence is browser localStorage except the leaderboard server. There are four independent stores, each guarded by HAS_DOM and wrapped in try/catch so a disabled or full store never crashes the game.
| Store | Key | Holds |
|---|---|---|
| Save/resume | terrapin_save_v1 |
the in-progress run |
| Achievements | terrapin_achievements_v1 |
unlocked-badge map |
| Hall (local fallback) | terrapin_hall_v1 |
per-browser leaderboard when there's no server |
| UI flags | terrapin_muted, terrapin_map_open, terrapin_music_open |
mute + sidebar state |
The save is a filtered snapshot
The interesting one is save/resume, because S holds things that must not be serialized — in-flight screen data, and fields that reference catalog objects with functions. The write strips them:
2214 function serializeSave(s) { 2215 var o = {}; 2216 Object.keys(s).forEach(function (k) { if (SAVE_DROP.indexOf(k) === -1) o[k] = s[k]; }); 2217 o.view = "lot"; 2218 return o; 2219 } 2220 function saveGame(s) { 2221 if (!HAS_DOM) return; 2222 try { window.localStorage.setItem(SAVE_KEY, JSON.stringify(serializeSave(s))); } catch (e) {} 2223 } 2224 function clearSave() { 2225 if (!HAS_DOM) return; 2226 try { window.localStorage.removeItem(SAVE_KEY); } catch (e) {}
SAVE_DROP (game.js:2091) lists the transient keys (currentEvent, mini, drive, peakData, peakQueued, result, and the transient blog UI state). Forcing view: "lot" means a resumed game always lands at the hub, never mid-animation or mid-event.
The write itself is triggered from one place — render() calls saveGame(S) whenever the lot is shown (see the state machine). That's the "clean point" guarantee: you only ever snapshot between shows, never mid-leg.
Restore rebuilds derived state
applySave(o) (game.js:2124) installs the stored object, nulls the transient fields, and — critically — re-runs recomputeBuffs so the shared CAP matches the loaded van (the economy gotcha in practice: the save stores S.vanmods, not the derived caps). loadSave validates the shape (o.role present, o.stop a number) before trusting it, so a corrupt or half-written blob is ignored rather than crashing the boot. The title screen shows Continue when hasSave() finds a valid unfinished run; the save is cleared on newGame and at endGame.
- Four localStorage stores, all
HAS_DOM-guarded and try/catch-wrapped. - The save is a filtered snapshot: drop transient keys, force
view:"lot", written only fromrender()at the lot. - Restore re-runs
recomputeBuffsto rebuildCAP, and validates shape before trusting a blob.