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; }