90 lines
2.2 KiB
TypeScript
90 lines
2.2 KiB
TypeScript
import {
|
|
combatantId,
|
|
createEncounter,
|
|
type Encounter,
|
|
isDomainError,
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
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<string, unknown>;
|
|
|
|
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[];
|
|
for (const c of combatants) {
|
|
if (typeof c !== "object" || c === null || Array.isArray(c)) return null;
|
|
const entry = c as Record<string, unknown>;
|
|
if (typeof entry.id !== "string") return null;
|
|
if (typeof entry.name !== "string") return null;
|
|
}
|
|
|
|
const rehydrated = combatants.map((c) => {
|
|
const entry = c as Record<string, unknown>;
|
|
const base = {
|
|
id: combatantId(entry.id as string),
|
|
name: entry.name as string,
|
|
initiative:
|
|
typeof entry.initiative === "number" ? entry.initiative : undefined,
|
|
};
|
|
|
|
// Validate AC field
|
|
const ac = entry.ac;
|
|
const validAc =
|
|
typeof ac === "number" && Number.isInteger(ac) && ac >= 0
|
|
? ac
|
|
: undefined;
|
|
|
|
// Validate and attach HP fields if valid
|
|
const maxHp = entry.maxHp;
|
|
const currentHp = entry.currentHp;
|
|
if (typeof maxHp === "number" && Number.isInteger(maxHp) && maxHp >= 1) {
|
|
const validCurrentHp =
|
|
typeof currentHp === "number" &&
|
|
Number.isInteger(currentHp) &&
|
|
currentHp >= 0 &&
|
|
currentHp <= maxHp;
|
|
return {
|
|
...base,
|
|
ac: validAc,
|
|
maxHp,
|
|
currentHp: validCurrentHp ? currentHp : maxHp,
|
|
};
|
|
}
|
|
|
|
return { ...base, ac: validAc };
|
|
});
|
|
|
|
const result = createEncounter(
|
|
rehydrated,
|
|
obj.activeIndex,
|
|
obj.roundNumber,
|
|
);
|
|
if (isDomainError(result)) return null;
|
|
|
|
return result;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|