131 lines
3.2 KiB
TypeScript
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;
|
|
}
|