import { describe, expect, it } from "vitest"; import type { ClassId } from "../app/characterCatalog"; import { classCanEquipItem, equipmentSlotForItem } from "./equipment"; import { compareEquipmentUpgrade, resolvedAverageItemLevel, resolvedEquipmentItems, resolvedEquipmentLoadout, type HeirloomContext, } from "./heirloomEquipment"; import { itemStatsFor } from "./itemStats"; import { LOOT_CATALOG, scaledLootItemLevel, type LootCatalogItem } from "./lootCatalog"; import type { InventoryItem } from "./lootTypes"; import { SPECIALIZATION_DEFINITIONS, type SpecializationDefinition, } from "./specializationCatalog"; import { useCombatStore } from "./combatStore"; import { normalizeInventory } from "./lootTypes"; const templates = LOOT_CATALOG.bosses.flatMap((boss) => boss.items); function context(profile: SpecializationDefinition, level: number, ownerKind: "player" | "party" = "player"): HeirloomContext { return { ownerId: ownerKind === "player" ? "player" : `party-${profile.id}`, ownerKind, classId: profile.classId, specializationId: profile.id, role: profile.role, level, }; } function catalogItem(template: LootCatalogItem, itemLevel: number, suffix = "test"): InventoryItem { return { instanceId: `${template.itemId}:${itemLevel}:${suffix}`, itemId: template.itemId, itemKey: template.itemKey, name: template.itemName, quality: template.quality, inventorySlot: template.inventorySlot, armorType: template.armorType, weaponType: template.weaponType, itemLevel, sourceItemLevel: template.sourceItemLevel, stats: itemStatsFor({ ...template, itemLevel }), quantity: 1, sourceBossKey: "test-boss", sourceBossName: "Test Boss", acquiredAt: 1, }; } function compatibleItems(classId: ClassId, level: number): InventoryItem[] { return templates .map((template, index) => catalogItem(template, level, String(index))) .filter((item) => classCanEquipItem(classId, item)); } describe("specialization-aware heirlooms", () => { it("caps player heirlooms at 79 and party heirlooms at 80", () => { const profile = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!; expect(new Set(resolvedEquipmentItems({}, [], context(profile, 80)).map((item) => item.itemLevel))).toEqual(new Set([79])); expect(new Set(resolvedEquipmentItems({}, [], context(profile, 80, "party")).map((item) => item.itemLevel))).toEqual(new Set([80])); expect(resolvedAverageItemLevel({}, [], context(profile, 40))).toBe(40); }); it("fills all applicable slots and uses the specialization weapon layout", () => { const protection = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "warrior:protection")!; const loadout = resolvedEquipmentLoadout({}, [], context(protection, 40)); expect(loadout["head"]?.quality).toBe("heirloom"); expect(loadout["finger"]?.quality).toBe("heirloom"); expect(loadout["trinket"]?.quality).toBe("heirloom"); expect(loadout["main-hand"]?.inventorySlot).toBe("one-hand"); expect(loadout["off-hand"]?.armorType).toBe("shield"); }); it("allocates only useful archetype stats", () => { const protection = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "warrior:protection")!; const holy = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!; const combat = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "rogue:combat")!; const protectionStats = resolvedEquipmentItems({}, [], context(protection, 40)).find((item) => item.inventorySlot === "chest")!.stats; const holyStats = resolvedEquipmentItems({}, [], context(holy, 40)).find((item) => item.inventorySlot === "chest")!.stats; const combatStats = resolvedEquipmentItems({}, [], context(combat, 40)).find((item) => item.inventorySlot === "chest")!.stats; expect(protectionStats).toMatchObject({ stamina: expect.any(Number), defenseRating: expect.any(Number), blockRating: expect.any(Number), spellPower: 0 }); expect(holyStats).toMatchObject({ intellect: expect.any(Number), spirit: expect.any(Number), spellPower: expect.any(Number), mp5: expect.any(Number), attackPower: 0 }); expect(combatStats).toMatchObject({ agility: expect.any(Number), attackPower: expect.any(Number), hitRating: expect.any(Number), spellPower: 0 }); expect(protectionStats.blockRating).toBeGreaterThan(0); expect(holyStats.spellPower).toBeGreaterThan(0); expect(combatStats.agility).toBeGreaterThan(0); }); it.each([1, 40, 79])("keeps compatible same-level boss items below heirlooms at level %i", (level) => { for (const profile of SPECIALIZATION_DEFINITIONS) { const loadout = resolvedEquipmentLoadout({}, [], context(profile, level)); for (const item of compatibleItems(profile.classId, level)) { expect(compareEquipmentUpgrade(item, loadout, profile, context(profile, level)).isUpgrade, `${profile.id}: ${item.name}`).toBe(false); } } }); it("opens a real item-level-85 upgrade path for every gear profile at level 80", () => { expect(scaledLootItemLevel(79)).toBe(79); expect(scaledLootItemLevel(80)).toBe(85); for (const profile of SPECIALIZATION_DEFINITIONS) { const loadout = resolvedEquipmentLoadout({}, [], context(profile, 80)); const upgrades = compatibleItems(profile.classId, 85) .filter((item) => compareEquipmentUpgrade(item, loadout, profile, context(profile, 80)).isUpgrade); expect(upgrades.length, profile.id).toBeGreaterThan(0); } }); it("lets earned assignments override a slot and restores its heirloom when removed", () => { const profile = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!; const ownerContext = context(profile, 80); const item = compatibleItems(profile.classId, 85).find((candidate) => equipmentSlotForItem(candidate) === "head") ?? compatibleItems(profile.classId, 85)[0]; const slot = equipmentSlotForItem(item)!; const equipped = resolvedEquipmentLoadout({ player: { [slot]: item.instanceId } }, [item], ownerContext); expect(equipped[slot]).toBe(item); expect(resolvedEquipmentLoadout({}, [item], ownerContext)[slot]?.quality).toBe("heirloom"); }); it("preserves player health and resource percentages across gear maximum changes", () => { const profile = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!; useCombatStore.getState().initializeCharacter({ classId: "priest", specializationId: profile.id, role: profile.role, level: 40, }); const item = compatibleItems("priest", 85).find((candidate) => equipmentSlotForItem(candidate) === "head") ?? compatibleItems("priest", 85)[0]; const before = useCombatStore.getState(); useCombatStore.setState({ health: before.maxHealth * 0.4, resource: before.maxResource * 0.25, inventory: [item], }); expect(useCombatStore.getState().equipItem(item.instanceId)).toBe(true); expect(useCombatStore.getState().health / useCombatStore.getState().maxHealth).toBeCloseTo(0.4, 5); expect(useCombatStore.getState().resource / useCombatStore.getState().maxResource).toBeCloseTo(0.25, 5); }); it("raises persisted item safety to 255 without admitting virtual heirlooms", () => { const persisted = { ...compatibleItems("priest", 85)[0], itemLevel: 999 }; expect(normalizeInventory([persisted])[0].itemLevel).toBe(255); expect(normalizeInventory([{ ...persisted, quality: "heirloom" }])).toEqual([]); }); });