Files
2D6-Dungeon/src/rules/persistence.ts

131 lines
3.2 KiB
TypeScript

import { z } from "zod";
import { runStateSchema } from "@/schemas/state";
import type { RunState } from "@/types/state";
export type StorageLike = {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
};
export type SavedRunRecord = {
id: string;
label: string;
savedAt: string;
run: RunState;
};
export type SavedRunSummary = {
id: string;
label: string;
savedAt: string;
phase: RunState["phase"];
currentLevel: number;
currentRoomId?: string;
adventurerName: string;
};
const STORAGE_KEY = "d2d6-dungeon.run-saves.v1";
const savedRunRecordSchema = z.object({
id: z.string().min(1),
label: z.string().min(1),
savedAt: z.string().min(1),
run: runStateSchema,
});
const savedRunRecordListSchema = z.array(savedRunRecordSchema);
function readSaveRecords(storage: StorageLike): SavedRunRecord[] {
const raw = storage.getItem(STORAGE_KEY);
if (!raw) {
return [];
}
const parsed = JSON.parse(raw) as unknown;
return savedRunRecordListSchema.parse(parsed);
}
function writeSaveRecords(storage: StorageLike, records: SavedRunRecord[]) {
storage.setItem(STORAGE_KEY, JSON.stringify(records));
}
function toSummary(record: SavedRunRecord): SavedRunSummary {
return {
id: record.id,
label: record.label,
savedAt: record.savedAt,
phase: record.run.phase,
currentLevel: record.run.currentLevel,
currentRoomId: record.run.currentRoomId,
adventurerName: record.run.adventurerSnapshot.name,
};
}
export function buildSaveLabel(run: RunState) {
const roomLabel = run.currentRoomId ?? "unknown-room";
return `${run.adventurerSnapshot.name} · L${run.currentLevel} · ${run.phase} · ${roomLabel}`;
}
export function listSavedRuns(storage: StorageLike): SavedRunSummary[] {
return readSaveRecords(storage)
.sort((left, right) => right.savedAt.localeCompare(left.savedAt))
.map(toSummary);
}
export function saveRun(
storage: StorageLike,
run: RunState,
options?: {
saveId?: string;
label?: string;
savedAt?: string;
},
): SavedRunSummary {
const savedAt = options?.savedAt ?? new Date().toISOString();
const id = options?.saveId ?? `save.${savedAt}`;
const label = options?.label ?? buildSaveLabel(run);
const record = savedRunRecordSchema.parse({
id,
label,
savedAt,
run,
});
const existing = readSaveRecords(storage).filter((entry) => entry.id !== id);
existing.unshift(record);
writeSaveRecords(storage, existing);
return toSummary(record);
}
export function loadSavedRun(storage: StorageLike, saveId: string): RunState {
const record = readSaveRecords(storage).find((entry) => entry.id === saveId);
if (!record) {
throw new Error(`Unknown save id: ${saveId}`);
}
return record.run;
}
export function deleteSavedRun(storage: StorageLike, saveId: string): SavedRunSummary[] {
const records = readSaveRecords(storage).filter((entry) => entry.id !== saveId);
writeSaveRecords(storage, records);
return records
.sort((left, right) => right.savedAt.localeCompare(left.savedAt))
.map(toSummary);
}
export function getBrowserStorage(): StorageLike | null {
if (typeof window === "undefined" || !window.localStorage) {
return null;
}
return window.localStorage;
}