import { describe, expect, it } from "vitest"; import type { InventoryItem } from "./lootTypes"; import { PLAYER_EQUIPMENT_OWNER_ID, classCanEquipItem, equipInventoryItem, equipmentItemLevel, equipmentStats, activeEquipmentStatsForClass, normalizeEquipment, unequipInventorySlot, } from "./equipment"; import { EMPTY_ITEM_STATS, type ItemStats } from "./itemStats"; import { deriveRomNativeStats } from "./romNativeStats"; function stats(overrides: Partial): ItemStats { return { ...EMPTY_ITEM_STATS, ...overrides }; } function item( instanceId: string, inventorySlot: string, overrides: Partial = {}, ): InventoryItem { return { instanceId, itemId: 1, itemKey: instanceId, name: instanceId, contentCategory: "wow", quality: "uncommon", inventorySlot, armorType: null, weaponType: null, itemLevel: 12, sourceItemLevel: 20, stats: stats({ spellPower: 3, stamina: 2 }), quantity: 1, sourceBossKey: "kresh", sourceBossName: "Kresh", acquiredAt: 1, ...overrides, }; } describe("equipment assignments", () => { it("moves a unique item between the player and companions", () => { const cloak = item("cloak", "back"); const player = equipInventoryItem({}, [cloak], PLAYER_EQUIPMENT_OWNER_ID, "priest", cloak.instanceId); expect(player.ok).toBe(true); expect(player.assignments.player?.back).toBe("cloak"); const companion = equipInventoryItem(player.assignments, [cloak], "party-damage-1-mage", "mage", cloak.instanceId); expect(companion.ok).toBe(true); expect(companion.assignments.player).toBeUndefined(); expect(companion.assignments["party-damage-1-mage"]?.back).toBe("cloak"); }); it("enforces armor, shield, and weapon compatibility", () => { const leather = item("leather", "chest", { armorType: "leather" }); const shield = item("shield", "off-hand", { armorType: "shield" }); const bow = item("bow", "ranged", { weaponType: "bow" }); expect(classCanEquipItem("priest", leather)).toBe(false); expect(classCanEquipItem("warrior", leather)).toBe(true); expect(classCanEquipItem("mage", shield)).toBe(false); expect(classCanEquipItem("shaman", shield)).toBe(true); expect(classCanEquipItem("hunter", bow)).toBe(true); expect(classCanEquipItem("warlock", bow)).toBe(false); }); it("rejects foreign, unresolved, and non-native RoM gear before changing assignments", () => { const wowDagger = item("wow-dagger", "one-hand", { weaponType: "dagger" }); const nativeRomDagger = item("rom-dagger", "one-hand", { contentCategory: "rom", weaponType: "dagger", stats: stats({ strength: 12, romNative: { strength: 12 } }), }); const unresolvedRomDagger = item("unresolved-rom-dagger", "one-hand", { contentCategory: null, weaponType: "dagger", stats: stats({ strength: 12 }), }); const foreign = equipInventoryItem({ player: { back: "kept" } }, [wowDagger], "player", "rom-priest", wowDagger.instanceId); expect(foreign).toEqual({ ok: false, assignments: { player: { back: "kept" } }, reason: "That item belongs to a different game category.", }); expect(equipInventoryItem({}, [nativeRomDagger], "player", "rom-priest", nativeRomDagger.instanceId).ok).toBe(true); expect(equipInventoryItem({}, [unresolvedRomDagger], "player", "rom-priest", unresolvedRomDagger.instanceId).ok).toBe(false); expect(normalizeEquipment({ player: { "main-hand": wowDagger.instanceId } }, [wowDagger], "rom")).toEqual({}); const forgedCategory = equipInventoryItem( { player: { back: "kept" } }, [wowDagger], "player", "rom-priest", wowDagger.instanceId, { ownerCategory: "wow" }, ); expect(forgedCategory.ok).toBe(false); expect(forgedCategory.assignments).toEqual({ player: { back: "kept" } }); expect(classCanEquipItem("rom-priest", wowDagger, { ownerCategory: "wow" })).toBe(false); }); it("does not apply foreign or unresolved items in active equipment stat reads", () => { const romItem = item("rom-native", "back", { contentCategory: "rom", stats: stats({ stamina: 20, romNative: { stamina: 20 } }), }); const wowItem = item("wow-foreign", "head", { stats: stats({ stamina: 90 }) }); const assignments = { player: { back: romItem.instanceId, head: wowItem.instanceId } }; expect(activeEquipmentStatsForClass(assignments, [romItem, wowItem], "player", "rom-priest", "rom")) .toMatchObject({ stamina: 20 }); expect(activeEquipmentStatsForClass(assignments, [romItem, wowItem], "player", "rom-priest", "coa")) .toMatchObject({ stamina: 0 }); expect(activeEquipmentStatsForClass(assignments, [romItem, wowItem], "player", "rom-priest", "wow")) .toEqual(EMPTY_ITEM_STATS); }); it("handles two-handed conflicts, unequipping, normalization, and item level", () => { const staff = item("staff", "two-hand", { weaponType: "staff", itemLevel: 18 }); const shield = item("shield", "off-hand", { armorType: "shield", itemLevel: 14 }); const main = equipInventoryItem({}, [staff, shield], "player", "warrior", staff.instanceId); const offhand = equipInventoryItem(main.assignments, [staff, shield], "player", "warrior", shield.instanceId); expect(offhand.assignments.player?.["main-hand"]).toBeUndefined(); expect(offhand.assignments.player?.["off-hand"]).toBe("shield"); expect(equipmentItemLevel(offhand.assignments, [staff, shield], "player")).toBe(14); const unequipped = unequipInventorySlot(offhand.assignments, "player", "off-hand"); expect(unequipped.player).toBeUndefined(); expect(normalizeEquipment({ player: { back: "missing", "off-hand": "shield" } }, [shield])) .toEqual({ player: { "off-hand": "shield" } }); }); it("totals only the items worn by the selected owner", () => { const cloak = item("cloak", "back", { stats: stats({ spellPower: 3, stamina: 4, armor: 9 }) }); const staff = item("staff", "two-hand", { weaponType: "staff", stats: stats({ strength: 8, stamina: 2 }), }); const assignments = { player: { back: cloak.instanceId }, companion: { "main-hand": staff.instanceId }, }; expect(equipmentStats(assignments, [cloak, staff], "player")) .toMatchObject({ spellPower: 3, stamina: 4, armor: 9 }); expect(equipmentStats(assignments, [cloak, staff], "companion")) .toMatchObject({ strength: 8, stamina: 2, armor: 0 }); }); it("bridges foreign direct stats per item while native payloads take precedence", () => { const sourceNative = item("native", "head", { stats: stats({ strength: 40, health: 99, mana: 99, romNative: { strength: 3, health: 5, mana: 5 } }), }); const foreign = item("foreign", "back", { stats: stats({ strength: 2, health: 10, mana: 10, spirit: 700, attackPower: 500, armor: 100, spellPower: 600 }), }); const inventory = [sourceNative, foreign]; const assignments = { player: { head: sourceNative.instanceId, back: foreign.instanceId } }; const equipped = equipmentStats(assignments, inventory, "player"); expect(equipped).toMatchObject({ strength: 42, health: 109, mana: 109 }); expect(equipped.romNative).toEqual({ strength: 5, health: 15, mana: 15 }); const withoutGear = deriveRomNativeStats({ classId: "rom-warrior", level: 1 }); const withGear = deriveRomNativeStats({ classId: "rom-warrior", level: 1, gear: equipped.romNative }); expect(withGear.attributes.strength - withoutGear.attributes.strength).toBe(5); expect(withGear.physicalAttack - withoutGear.physicalAttack).toBe(10); expect(withGear.health.max - withoutGear.health.max).toBeCloseTo(16); expect(withGear.attributes.wisdom).toBe(withoutGear.attributes.wisdom); const mageWithoutGear = deriveRomNativeStats({ classId: "rom-mage", level: 1 }); const mageWithGear = deriveRomNativeStats({ classId: "rom-mage", level: 1, gear: equipped.romNative }); expect(mageWithGear.mana.max - mageWithoutGear.mana.max).toBe(15); const unequipped = equipmentStats({}, inventory, "player"); expect(unequipped.romNative).toBeUndefined(); expect(deriveRomNativeStats({ classId: "rom-warrior", level: 1, gear: unequipped.romNative }).health.max) .toBe(withoutGear.health.max); }); it("keeps CoA-tagged direct stats out of the RoM native projection", () => { const coaGear = item("coa-gear", "chest", { contentCategory: "coa", armorType: "plate", stats: stats({ stamina: 7, strength: 3 }), }); const active = activeEquipmentStatsForClass( { player: { chest: coaGear.instanceId } }, [coaGear], "player", "guardian", "coa", ); expect(active).toMatchObject({ stamina: 7, strength: 3 }); expect(active.romNative).toBeUndefined(); }); });