88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
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;
|
|
}
|