feat: bootstrap walking skeleton
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { del, get, set } from 'idb-keyval';
|
||||
import type { Content } from '../content/schema';
|
||||
import { createGameState, type GameState } from '../engine/game';
|
||||
import { applyOfflineProgress, createSave, deserializeSave, serializeSave } from '../engine/save';
|
||||
|
||||
/**
|
||||
* Persistence adapter (IO layer — deliberately outside the pure engine).
|
||||
*
|
||||
* A `SaveBackend` is a pluggable string store; the orchestration (`loadGame`,
|
||||
* `saveGame`) turns it into typed game state via the engine's save module and
|
||||
* credits offline progress on boot. The real backend prefers IndexedDB
|
||||
* (idb-keyval) and falls back to localStorage, then to an in-memory store so the
|
||||
* game still runs (without persistence) in hostile environments.
|
||||
*/
|
||||
|
||||
export const SAVE_KEY = 'idlegame:save:v1';
|
||||
|
||||
export interface SaveBackend {
|
||||
load(): Promise<string | null>;
|
||||
save(serialized: string): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
export function createMemoryBackend(initial: string | null = null): SaveBackend {
|
||||
let value = initial;
|
||||
return {
|
||||
load: () => Promise.resolve(value),
|
||||
save: (serialized) => {
|
||||
value = serialized;
|
||||
return Promise.resolve();
|
||||
},
|
||||
clear: () => {
|
||||
value = null;
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createLocalStorageBackend(key: string): SaveBackend {
|
||||
return {
|
||||
load: () => Promise.resolve(globalThis.localStorage.getItem(key)),
|
||||
save: (serialized) => {
|
||||
globalThis.localStorage.setItem(key, serialized);
|
||||
return Promise.resolve();
|
||||
},
|
||||
clear: () => {
|
||||
globalThis.localStorage.removeItem(key);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createIdbBackend(key: string): SaveBackend {
|
||||
return {
|
||||
load: async () => (await get<string>(key)) ?? null,
|
||||
save: (serialized) => set(key, serialized),
|
||||
clear: () => del(key),
|
||||
};
|
||||
}
|
||||
|
||||
/** Choose the best available backend for the current environment. */
|
||||
export function createDefaultBackend(key: string = SAVE_KEY): SaveBackend {
|
||||
if (typeof indexedDB !== 'undefined') {
|
||||
return createIdbBackend(key);
|
||||
}
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
return createLocalStorageBackend(key);
|
||||
}
|
||||
return createMemoryBackend();
|
||||
}
|
||||
|
||||
export interface LoadResult {
|
||||
state: GameState;
|
||||
/** Milliseconds of offline time credited on this load (0 for a fresh game). */
|
||||
offlineMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and hydrate game state, crediting offline progress. A missing or corrupt
|
||||
* save yields a fresh game rather than throwing — never block boot on bad data.
|
||||
*/
|
||||
export async function loadGame(
|
||||
content: Content,
|
||||
backend: SaveBackend,
|
||||
now: number,
|
||||
): Promise<LoadResult> {
|
||||
const raw = await backend.load();
|
||||
if (!raw) {
|
||||
return { state: createGameState(content), offlineMs: 0 };
|
||||
}
|
||||
|
||||
let savedAt: number;
|
||||
let state: GameState;
|
||||
try {
|
||||
const save = deserializeSave(raw);
|
||||
const base = createGameState(content);
|
||||
const activeActionId =
|
||||
save.state.activeActionId && content.actionsById[save.state.activeActionId]
|
||||
? save.state.activeActionId
|
||||
: null;
|
||||
state = {
|
||||
resources: { ...base.resources, ...save.state.resources },
|
||||
activeActionId,
|
||||
actionElapsedMs: save.state.actionElapsedMs,
|
||||
};
|
||||
savedAt = save.savedAt;
|
||||
} catch {
|
||||
return { state: createGameState(content), offlineMs: 0 };
|
||||
}
|
||||
|
||||
const offlineMs = applyOfflineProgress(state, content, savedAt, now);
|
||||
return { state, offlineMs };
|
||||
}
|
||||
|
||||
export async function saveGame(state: GameState, backend: SaveBackend, now: number): Promise<void> {
|
||||
await backend.save(serializeSave(createSave(state, now)));
|
||||
}
|
||||
Reference in New Issue
Block a user