Lesson 08
Save and load: keep progress without forcing accounts.
Many browser games do not need login. `localStorage` is enough for best scores, unlocked levels, settings and single-player progress. The important part is saving the right data and surviving bad or old saves.
1. Save only what must persist
Moonlight Diner does not save every active customer or animation. It saves campaign progress, chosen difficulty, upgrades and story flags. That keeps the save small and reduces the chance of loading into a broken mid-action state.
function saveGame() {
try {
var saveData = {
difficulty: state.difficulty,
shift: state.shift,
mode: state.mode,
totalServed: state.totalServed,
upgrades: state.upgrades,
calFed: state.calFed,
rowanFed: state.rowanFed,
cratesMade: state.cratesMade
};
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
} catch(e) {}
}
Source pattern: Moonlight Diner.
This is a good default for longer games: save progress between sessions, not necessarily every moving object on screen. If the game is an RPG or mystery, save rooms, inventory and solved puzzles. If it is an arcade game, save best score and settings.
2. Load defensively
Users can clear storage, browser extensions can block storage, old versions can contain missing fields and a bad JSON value can throw an error. Loading should fail safely and let the player start a new game.
function loadGame() {
try {
var raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
var d = JSON.parse(raw);
state.difficulty = d.difficulty || "normal";
state.shift = d.shift || 0;
state.mode = d.mode || "campaign";
state.totalServed = d.totalServed || 0;
state.upgrades = d.upgrades || [];
return true;
} catch(e) {
return false;
}
}
The fallback values matter. If `d.upgrades` is missing, an empty array is safe. If difficulty is missing, normal mode is safe. Never assume a save created last week perfectly matches today's code.
3. Version saves for story games
Archive 9 uses a version field. That is useful for games with multiple rooms, evidence and puzzle state, because structure can change during development.
function saveGame() {
if (S.phase === "start" || S.phase === "ending") return;
var data = {
v: 1,
room: S.room,
unlocked: S.unlocked,
evidence: S.evidence,
theses: S.theses,
puzzles: S.puzzles,
dialogues: S.dialogues,
ending: S.ending
};
try { localStorage.setItem(SAVE, JSON.stringify(data)); } catch(e) {}
}
function loadGame() {
try {
var raw = localStorage.getItem(SAVE);
if (!raw) return false;
var data = JSON.parse(raw);
if (!data || data.v !== 1) return false;
S.room = data.room || "act1";
S.unlocked = data.unlocked || ["act1"];
S.evidence = data.evidence || [];
return true;
} catch(e) { return false; }
}
Source pattern: Archive 9: The Missing Hour.
If you later change the save format, you can support `v: 2` while still rejecting unknown versions cleanly.
4. Add reset and continue intentionally
A continue button should appear only if a save exists. A reset button should ask for confirmation before deleting progress. This is not only user-friendly; it also helps QA test new game starts after a save exists.
function hasSave() {
try { return !!localStorage.getItem(SAVE_KEY); }
catch(e) { return false; }
}
resetButton.addEventListener("click", function() {
if (confirm("Reset all progress?")) {
localStorage.removeItem(SAVE_KEY);
startNewGame();
}
});
For short arcade games, reset may be unnecessary. For story games and management games, it is essential. Players should not need developer tools to restart.
5. What not to store
- Do not store secrets. localStorage is readable by scripts running on the page.
- Do not store huge generated maps if a seed can recreate them.
- Do not store every particle, animation timer or temporary toast message.
- Do not rely on localStorage for global ratings or comments. Those need a server or API.
- Do not crash when storage fails. Private modes and strict browsers can behave differently.
6. Save migrations
If you keep improving a game, old saves will eventually miss new fields. A migration function lets you upgrade older data into the current shape instead of throwing it away. Keep migrations small and explicit.
function migrateSave(data) {
if (!data || typeof data !== "object") return null;
if (!data.v) {
data.v = 1;
data.settings = data.settings || { sound: true };
}
if (data.v === 1) {
data.v = 2;
data.unlockedSkins = data.unlockedSkins || [];
}
return data.v === 2 ? data : null;
}
This pattern is useful when a story game adds a new room, a platformer adds collectibles or a management game adds difficulty. Without migration, players can lose progress after every update. With migration, you can keep improving the game while respecting returning players.
7. Test saves like a feature
Save systems deserve their own QA pass. Start a new game, make progress, reload the page, continue, reset and start again. Then try a corrupted save by temporarily writing invalid JSON in the console. The game should not crash.
localStorage.setItem(SAVE_KEY, "{broken json");
console.assert(loadGame() === false, "Broken saves should fail safely");
Also test what happens when storage is unavailable. The game should still be playable even if progress cannot be saved. For best scores and settings, failure can be silent. For longer games, show a small warning only if the user expects progress to persist.