Files
Country-Game---The-Desk/web/js/save/storage.js
T

118 lines
3.3 KiB
JavaScript

// Browser storage for the autosave, the manual slots and the settings.
// Every access is wrapped: if localStorage is missing or throws (private window, blocked
// site data, full quota) the game keeps working and uses memory for the rest of the session.
export const SLOT_COUNT = 3;
export const KEYS = {
auto: "cg.autosave",
slot: (n) => `cg.slot.${n}`,
settings: "cg.settings",
};
function browserStorage() {
try {
const probe = "cg.probe";
globalThis.localStorage.setItem(probe, "1");
globalThis.localStorage.removeItem(probe);
return globalThis.localStorage;
} catch {
return null;
}
}
// backend: anything with getItem/setItem/removeItem, for tests. Default: localStorage.
export function createStorage(backend = browserStorage()) {
const memory = new Map();
const store = { persistent: backend !== null };
store.get = (key) => {
try {
if (backend) return backend.getItem(key);
} catch {
store.persistent = false;
}
return memory.get(key) ?? null;
};
// Returns false when the write could not be made durable.
store.set = (key, value) => {
try {
if (backend) {
backend.setItem(key, value);
return true;
}
} catch {
store.persistent = false;
}
memory.set(key, value);
return false;
};
store.remove = (key) => {
memory.delete(key);
try {
backend?.removeItem(key);
} catch {
store.persistent = false;
}
};
return store;
}
// ---- Saved games: { meta, state } under one key ----
export function writeSave(store, key, state) {
const meta = {
savedAt: Date.now(),
day: state.day,
countryName: state.countryName,
pronouns: state.pronouns,
scenarioId: state.scenarioId,
over: state.over?.type ?? null,
};
return store.set(key, JSON.stringify({ meta, state }));
}
// Returns { meta, state } or null when the slot is empty or unreadable.
export function readSave(store, key) {
const raw = store.get(key);
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
return parsed?.state && parsed?.meta ? parsed : null;
} catch {
return null;
}
}
// ---- Settings ----
// The mixer: one level (0 to 1) per sound group and one for the music. Keep in step with MIX_GROUPS in ui/audio.js.
export const MIX_KEYS = ["stamp", "paper", "page", "sign", "telegram", "alert", "chime", "tick", "countdown", "click", "ending", "music"];
export const DEFAULT_MIX = Object.fromEntries(MIX_KEYS.map((k) => [k, k === "music" ? 0.5 : 1]));
const DEFAULT_SETTINGS = { lang: null, slow: false, sound: true, volume: 0.6, mix: DEFAULT_MIX, musicOn: true };
const level = (v, fallback) => (typeof v === "number" && v >= 0 && v <= 1 ? v : fallback);
export function loadSettings(store) {
try {
const s = JSON.parse(store.get(KEYS.settings) ?? "{}");
return {
lang: s.lang === "en" || s.lang === "fr" ? s.lang : DEFAULT_SETTINGS.lang,
slow: s.slow === true,
sound: s.sound !== false,
volume: level(s.volume, DEFAULT_SETTINGS.volume),
mix: Object.fromEntries(MIX_KEYS.map((k) => [k, level(s.mix?.[k], DEFAULT_MIX[k])])),
musicOn: s.musicOn !== false,
};
} catch {
return { ...DEFAULT_SETTINGS, mix: { ...DEFAULT_MIX } };
}
}
export function saveSettings(store, settings) {
return store.set(KEYS.settings, JSON.stringify(settings));
}