import { DeviceClock, GameEngine, IndexedDbBackend, MemoryBackend, ResolutionLoop, SaveStorage, TabLock, type LockManagerLike, type LoopHost, } from "@deadhead/engine"; /** * The game on this device (CR-014): save storage, the device clock, the tab * lock and the engine, wired to the browser. Native storage and notifications * join in Step 14.9. */ export interface Game { engine: GameEngine; lock: TabLock; storage: SaveStorage; clock: DeviceClock; devTools: boolean; } /** Developer tools: development builds, and test builds made with VITE_DEV_TOOLS=true. */ export const DEV_TOOLS = !import.meta.env.PROD || import.meta.env.VITE_DEV_TOOLS === "true"; const DEV_OFFSET_KEY = "deadhead:dev-clock-offset"; function savedDevOffset(): number { try { const value = Number(localStorage.getItem(DEV_OFFSET_KEY)); return Number.isFinite(value) && value > 0 ? value : 0; } catch { return 0; } } export function createGame(): Game { const storage = new SaveStorage( typeof indexedDB === "undefined" ? new MemoryBackend() : new IndexedDbBackend(indexedDB), ); const clock = new DeviceClock(Date.now, { offsetMs: DEV_TOOLS ? savedDevOffset() : 0 }); if (DEV_TOOLS) { // The developer clock offset survives a reload (test builds fast-forward, then reload). const advance = clock.advance.bind(clock); clock.advance = (ms) => { advance(ms); try { localStorage.setItem(DEV_OFFSET_KEY, String(clock.offsetMs)); } catch { // Storage blocked: the offset lasts until the page reloads. } }; } const locks = (navigator as Navigator & { locks?: LockManagerLike }).locks; const lock = new TabLock(locks); const engine = new GameEngine({ storage, clock, canWrite: () => lock.held, devTools: DEV_TOOLS, // Responses are checked in development, as the server did outside production. validateResponses: !import.meta.env.PROD, }); return { engine, lock, storage, clock, devTools: DEV_TOOLS }; } /** The browser as the loop's host: a timer, and the page becoming visible again. */ export const browserLoopHost: LoopHost = { setInterval: (fn, ms) => window.setInterval(fn, ms), clearInterval: (handle) => window.clearInterval(handle as number), onResume(fn) { const listener = () => { if (document.visibilityState === "visible") fn(); }; document.addEventListener("visibilitychange", listener); return () => document.removeEventListener("visibilitychange", listener); }, }; export function startResolutionLoop(game: Game, onChange: () => void): ResolutionLoop { const loop = new ResolutionLoop(game.engine, browserLoopHost, onChange); void loop.start(); return loop; }