/** * Local-storage facade used by: * - Level unlock state (req 17.1) * - Floating control layout (req 17.2) * - BGM / SFX volume (req 17.3, 16.4) * - Tutorial completion flags (req 17.4) * - Story-intro seen flag (req 17.5 / 19.5) * * Rationale for the thin facade: * - The WeChat Mini Game runtime exposes `wx.setStorageSync` while the * in-editor / browser preview exposes `sys.localStorage`. We isolate * both behind a single `IStorageDriver` interface so that switching * platforms is a single line. * - On read, a failure never throws: it returns the provided default value * (req 17.6 — "must not crash if local storage is unreadable"). * - All values go through JSON serialisation so that structured objects * round-trip without callers having to remember to stringify. */ export interface IStorageDriver { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; } /** Selects the best available driver at runtime. */ function detectDriver(): IStorageDriver { // 1. WeChat Mini Game global const wxGlobal = (globalThis as any).wx; if (wxGlobal && typeof wxGlobal.setStorageSync === 'function') { return { getItem(key) { try { const v = wxGlobal.getStorageSync(key); return v === '' ? null : (v as string); } catch { return null; } }, setItem(key, value) { try { wxGlobal.setStorageSync(key, value); } catch { // swallow — req 17.6 } }, removeItem(key) { try { wxGlobal.removeStorageSync(key); } catch { // swallow } }, }; } // 2. Browser localStorage if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) { const ls = (globalThis as any).localStorage as Storage; return { getItem: (k) => ls.getItem(k), setItem: (k, v) => ls.setItem(k, v), removeItem: (k) => ls.removeItem(k), }; } // 3. In-memory fallback (Jest, Node-only unit tests). const mem = new Map(); return { getItem: (k) => (mem.has(k) ? (mem.get(k) as string) : null), setItem: (k, v) => { mem.set(k, v); }, removeItem: (k) => { mem.delete(k); }, }; } export class StorageMgr { private driver: IStorageDriver; constructor(driver?: IStorageDriver) { this.driver = driver ?? detectDriver(); } /** * Read a JSON-serialisable value. Returns `defaultValue` if the key is * missing, unparseable, or the underlying driver throws (req 17.6). */ public get(key: string, defaultValue: T): T { try { const raw = this.driver.getItem(key); if (raw == null) { return defaultValue; } return JSON.parse(raw) as T; } catch { return defaultValue; } } /** Write a JSON-serialisable value. Silently ignores driver errors. */ public set(key: string, value: T): void { try { this.driver.setItem(key, JSON.stringify(value)); } catch { // req 17.6 } } public remove(key: string): void { this.driver.removeItem(key); } /** Swap the driver at runtime. Used in unit tests and platform ports. */ public setDriver(driver: IStorageDriver): void { this.driver = driver; } } /** Shared project-wide storage manager. */ export const globalStorageMgr = new StorageMgr();