110 lines
2.8 KiB
JavaScript
110 lines
2.8 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 ----
|
|
|
|
const DEFAULT_SETTINGS = { lang: null, slow: false, sound: true, volume: 0.6 };
|
|
|
|
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: typeof s.volume === "number" && s.volume >= 0 && s.volume <= 1 ? s.volume : DEFAULT_SETTINGS.volume,
|
|
};
|
|
} catch {
|
|
return { ...DEFAULT_SETTINGS };
|
|
}
|
|
}
|
|
|
|
export function saveSettings(store, settings) {
|
|
return store.set(KEYS.settings, JSON.stringify(settings));
|
|
}
|