✨Feature: add town systems, saves, recovery, and level progression
This commit is contained in:
130
src/rules/persistence.ts
Normal file
130
src/rules/persistence.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user