import { normalizeItemStats, MAX_PERSISTED_ITEM_LEVEL, type ItemQuality, type ItemStats, } from "./itemStats"; export type { ItemQuality, ItemStats } from "./itemStats"; /** A concrete item owned by a character, distinct from its SQL item template. */ export interface InventoryItem { readonly instanceId: string; readonly itemId: number; readonly itemKey: string; readonly name: string; readonly quality: ItemQuality; readonly inventorySlot: string; readonly armorType: string | null; readonly weaponType: string | null; /** Scaled at boss death from the killing player's level. */ readonly itemLevel: number; /** Original 3.3.5a item level retained for provenance and future balancing. */ readonly sourceItemLevel: number; /** Concrete scaled attributes captured when the boss drops this item. */ readonly stats: ItemStats; readonly quantity: number; readonly sourceBossKey: string; readonly sourceBossName: string; readonly acquiredAt: number; } const ITEM_QUALITIES = new Set(["common", "uncommon", "rare", "epic", "legendary"]); /** Safely migrates persisted profiles created before inventory existed. */ export function normalizeInventory(value: unknown): InventoryItem[] { if (!Array.isArray(value)) return []; const normalized: InventoryItem[] = []; for (const candidate of value) { if (!candidate || typeof candidate !== "object") continue; const item = candidate as Partial; if ( typeof item.instanceId !== "string" || !Number.isFinite(item.itemId) || typeof item.itemKey !== "string" || typeof item.name !== "string" || !ITEM_QUALITIES.has(item.quality as ItemQuality) || typeof item.inventorySlot !== "string" || !Number.isFinite(item.itemLevel) || !Number.isFinite(item.sourceItemLevel) || !Number.isFinite(item.quantity) || typeof item.sourceBossKey !== "string" || typeof item.sourceBossName !== "string" || !Number.isFinite(item.acquiredAt) ) continue; const itemLevel = Math.max(1, Math.min(MAX_PERSISTED_ITEM_LEVEL, Math.trunc(item.itemLevel!))); normalized.push({ instanceId: item.instanceId, itemId: Math.max(1, Math.trunc(item.itemId!)), itemKey: item.itemKey, name: item.name, quality: item.quality as ItemQuality, inventorySlot: item.inventorySlot, armorType: typeof item.armorType === "string" ? item.armorType : null, weaponType: typeof item.weaponType === "string" ? item.weaponType : null, itemLevel, sourceItemLevel: Math.max(1, Math.trunc(item.sourceItemLevel!)), stats: normalizeItemStats(item.stats, { itemId: Math.max(1, Math.trunc(item.itemId!)), quality: item.quality as ItemQuality, inventorySlot: item.inventorySlot, armorType: typeof item.armorType === "string" ? item.armorType : null, weaponType: typeof item.weaponType === "string" ? item.weaponType : null, itemLevel, sourceItemLevel: Math.max(1, Math.trunc(item.sourceItemLevel!)), }), quantity: Math.max(1, Math.trunc(item.quantity!)), sourceBossKey: item.sourceBossKey, sourceBossName: item.sourceBossName, acquiredAt: Math.max(0, Math.trunc(item.acquiredAt!)), }); } return normalized; }