import { type ConditionId, combatantId, createEncounter, creatureId, type Encounter, isDomainError, playerCharacterId, VALID_CONDITION_IDS, VALID_PLAYER_COLORS, VALID_PLAYER_ICONS, } from "@initiative/domain"; const STORAGE_KEY = "initiative:encounter"; export function saveEncounter(encounter: Encounter): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(encounter)); } catch { // Silently swallow errors (quota exceeded, storage unavailable) } } function validateAc(value: unknown): number | undefined { return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined; } function validateConditions(value: unknown): ConditionId[] | undefined { if (!Array.isArray(value)) return undefined; const valid = value.filter( (v): v is ConditionId => typeof v === "string" && VALID_CONDITION_IDS.has(v), ); return valid.length > 0 ? valid : undefined; } function validateCreatureId(value: unknown) { return typeof value === "string" && value.length > 0 ? creatureId(value) : undefined; } function validateHp( rawMaxHp: unknown, rawCurrentHp: unknown, ): { maxHp: number; currentHp: number } | undefined { if ( typeof rawMaxHp !== "number" || !Number.isInteger(rawMaxHp) || rawMaxHp < 1 ) { return undefined; } const validCurrentHp = typeof rawCurrentHp === "number" && Number.isInteger(rawCurrentHp) && rawCurrentHp >= 0 && rawCurrentHp <= rawMaxHp; return { maxHp: rawMaxHp, currentHp: validCurrentHp ? rawCurrentHp : rawMaxHp, }; } function rehydrateCombatant(c: unknown) { const entry = c as Record; const base = { id: combatantId(entry.id as string), name: entry.name as string, initiative: typeof entry.initiative === "number" ? entry.initiative : undefined, }; const color = typeof entry.color === "string" && VALID_PLAYER_COLORS.has(entry.color) ? entry.color : undefined; const icon = typeof entry.icon === "string" && VALID_PLAYER_ICONS.has(entry.icon) ? entry.icon : undefined; const pcId = typeof entry.playerCharacterId === "string" && entry.playerCharacterId.length > 0 ? playerCharacterId(entry.playerCharacterId) : undefined; const shared = { ...base, ac: validateAc(entry.ac), conditions: validateConditions(entry.conditions), isConcentrating: entry.isConcentrating === true ? true : undefined, creatureId: validateCreatureId(entry.creatureId), color, icon, playerCharacterId: pcId, }; const hp = validateHp(entry.maxHp, entry.currentHp); return hp ? { ...shared, ...hp } : shared; } function isValidCombatantEntry(c: unknown): boolean { if (typeof c !== "object" || c === null || Array.isArray(c)) return false; const entry = c as Record; return typeof entry.id === "string" && typeof entry.name === "string"; } export function loadEncounter(): Encounter | null { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw === null) return null; const parsed: unknown = JSON.parse(raw); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null; const obj = parsed as Record; if (!Array.isArray(obj.combatants)) return null; if (typeof obj.activeIndex !== "number") return null; if (typeof obj.roundNumber !== "number") return null; const combatants = obj.combatants as unknown[]; // Handle empty encounter (cleared state) directly — createEncounter rejects empty arrays if (combatants.length === 0) { return { combatants: [], activeIndex: 0, roundNumber: 1, }; } if (!combatants.every(isValidCombatantEntry)) return null; const rehydrated = combatants.map(rehydrateCombatant); const result = createEncounter( rehydrated, obj.activeIndex, obj.roundNumber, ); if (isDomainError(result)) return null; return result; } catch { return null; } }