import type { ConditionId } from "./conditions.js"; import { VALID_CONDITION_IDS } from "./conditions.js"; import { creatureId } from "./creature-types.js"; import { playerCharacterId, VALID_PLAYER_COLORS, VALID_PLAYER_ICONS, } from "./player-character-types.js"; import type { Combatant } from "./types.js"; import { combatantId } from "./types.js"; 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 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 validateTempHp(value: unknown): number | undefined { return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined; } function validateInteger(value: unknown): number | undefined { return typeof value === "number" && Number.isInteger(value) ? value : undefined; } function validateSetMember( value: unknown, valid: ReadonlySet, ): string | undefined { return typeof value === "string" && valid.has(value) ? value : undefined; } function validateNonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } function parseOptionalFields(entry: Record) { return { initiative: validateInteger(entry.initiative), ac: validateAc(entry.ac), conditions: validateConditions(entry.conditions), isConcentrating: entry.isConcentrating === true ? true : undefined, creatureId: validateNonEmptyString(entry.creatureId) ? creatureId(entry.creatureId as string) : undefined, color: validateSetMember(entry.color, VALID_PLAYER_COLORS), icon: validateSetMember(entry.icon, VALID_PLAYER_ICONS), playerCharacterId: validateNonEmptyString(entry.playerCharacterId) ? playerCharacterId(entry.playerCharacterId as string) : undefined, tempHp: validateTempHp(entry.tempHp), }; } export function rehydrateCombatant(raw: unknown): Combatant | 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") return null; const shared: Combatant = { id: combatantId(entry.id), name: entry.name, ...parseOptionalFields(entry), }; const hp = validateHp(entry.maxHp, entry.currentHp); return hp ? { ...shared, ...hp } : shared; }