Add folder game, file browser UI, and automated tests.
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
This commit is contained in:
57
src/lib/folder-game/storage.ts
Normal file
57
src/lib/folder-game/storage.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user