Introduces a localStorage-backed messy-desktop challenge with sub-steps, drag-and-drop and Places/Back navigation, IBM Plex Mono, and a /api/health endpoint. Adds Vitest coverage for the API and level logic plus Playwright smoke tests. Made-with: Cursor
58 lines
1.5 KiB
TypeScript
58 lines
1.5 KiB
TypeScript
import type { SaveV1 } from "./types";
|
|
|
|
import {
|
|
LAST_PLAYABLE,
|
|
freshSave,
|
|
migrateSubIndex,
|
|
subCount,
|
|
} from "./levels";
|
|
|
|
export const STORAGE_KEY = "folder-game-challenge:v1";
|
|
|
|
export function parseSave(raw: string | null): SaveV1 | null {
|
|
if (!raw) return null;
|
|
try {
|
|
const v = JSON.parse(raw) as SaveV1;
|
|
if (v?.version !== 1) return null;
|
|
if (typeof v.levelIndex !== "number") return null;
|
|
if (v.levelIndex < 0 || v.levelIndex > LAST_PLAYABLE + 1) return null;
|
|
if (!v.fs?.nodes || typeof v.fs.viewParentId !== "string") return null;
|
|
if (!Array.isArray(v.fs.visitedFolderIds)) return null;
|
|
const subIndex = migrateSubIndex(
|
|
v.levelIndex,
|
|
v.fs,
|
|
typeof v.subIndex === "number" ? v.subIndex : undefined,
|
|
);
|
|
const maxSub = Math.max(0, subCount(v.levelIndex) - 1);
|
|
return { ...v, subIndex: Math.min(subIndex, maxSub) };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function loadSave(): SaveV1 | null {
|
|
if (typeof window === "undefined") return null;
|
|
return parseSave(localStorage.getItem(STORAGE_KEY));
|
|
}
|
|
|
|
export function writeSave(save: SaveV1) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(save));
|
|
}
|
|
|
|
export function clearSave() {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
}
|
|
|
|
export function resumeOrNew(): SaveV1 {
|
|
return loadSave() ?? freshSave(0);
|
|
}
|
|
|
|
export function hasInProgressSave(): boolean {
|
|
const s = loadSave();
|
|
return s !== null && s.levelIndex <= LAST_PLAYABLE;
|
|
}
|
|
|
|
export function isFinished(save: SaveV1) {
|
|
return save.levelIndex > LAST_PLAYABLE;
|
|
}
|