import type { PlayerCharacter } from "@initiative/domain"; import { playerCharacterId, VALID_PLAYER_COLORS, VALID_PLAYER_ICONS, } from "@initiative/domain"; const STORAGE_KEY = "initiative:player-characters"; export function savePlayerCharacters(characters: PlayerCharacter[]): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(characters)); } catch { // Silently swallow errors (quota exceeded, storage unavailable) } } function isValidOptionalMember( value: unknown, valid: ReadonlySet, ): boolean { return value === undefined || (typeof value === "string" && valid.has(value)); } function rehydrateCharacter(raw: unknown): PlayerCharacter | null { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null; const entry = raw as Record; if (typeof entry.id !== "string" || entry.id.length === 0) return null; if (typeof entry.name !== "string" || entry.name.trim().length === 0) return null; if ( typeof entry.ac !== "number" || !Number.isInteger(entry.ac) || entry.ac < 0 ) return null; if ( typeof entry.maxHp !== "number" || !Number.isInteger(entry.maxHp) || entry.maxHp < 1 ) return null; if (!isValidOptionalMember(entry.color, VALID_PLAYER_COLORS)) return null; if (!isValidOptionalMember(entry.icon, VALID_PLAYER_ICONS)) return null; return { id: playerCharacterId(entry.id), name: entry.name, ac: entry.ac, maxHp: entry.maxHp, color: entry.color as PlayerCharacter["color"], icon: entry.icon as PlayerCharacter["icon"], }; } export function loadPlayerCharacters(): PlayerCharacter[] { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw === null) return []; const parsed: unknown = JSON.parse(raw); if (!Array.isArray(parsed)) return []; const characters: PlayerCharacter[] = []; for (const item of parsed) { const pc = rehydrateCharacter(item); if (pc !== null) { characters.push(pc); } } return characters; } catch { return []; } }