import { describe, expect, it } from "vitest"; import { applyAura, type ActiveAura } from "./combatAuras"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog"; import { enemyWeaponDebuffAuraDefinition, enemyWeaponDebuffContract } from "./enemyWeaponDebuffs"; import { activeEnemyDisarmChannels, enemyDisarmAurasForWeaponChannel, enemyDisarmAbilityWeaponChannel, enemyDisarmAdjustedAbilityWeapon, enemyDisarmAdjustedWeaponStats, enemyDisarmBlocksAbilityCast, enemyDisarmBlocksNpcWeaponAbility, enemyDisarmItemDependentAuraSuppressionIds, enemyDisarmInvalidatingProfile, enemyDisarmRequirementCandidates, enemyDisarmTargetWeaponsFromLoadout, } from "./enemyDisarmRuntime"; const weapon = (weaponType: string, inventorySlot = "one-hand") => ({ weaponType, inventorySlot, stats: { ...EMPTY_ITEM_STATS, meleeWeapon: { damageMin: 10, damageMax: 20, damageSchool: "physical" as const, speedMs: 2_000 } }, }); function disarmAuras(profile: "ascension" | "epoch", targetId = "actor"): ActiveAura[] { const definition = enemyWeaponDebuffAuraDefinition(enemyWeaponDebuffContract(6713, profile)!); return [...applyAura([], { definition, sourceId: "mob", targetId, now: 100 }).auras]; } describe("source-bound character Disarm runtime predicates", () => { it("projects the corrected WotLK DBC equipped weapon requirement onto ranks", () => { const sinisterStrike = WOW335_ABILITIES_BY_CLASS.rogue.find((ability) => ability.name === "Sinister Strike"); expect(sinisterStrike?.ranks?.[0]).toMatchObject({ spellId: 1752, wowWeaponRequirements: { equippedItemClass: 2, equippedItemSubclassMask: 173_555, equippedItemInventoryMask: 0, }, }); expect(WOW335_ABILITIES_BY_CLASS.rogue.find((ability) => ability.name === "Eviscerate")?.ranks?.[0] ?.wowWeaponRequirements).toMatchObject({ equippedItemClass: 2 }); }); it("derives active channels by target and expiration without recognizing unrelated/native tags", () => { const ascension = disarmAuras("ascension"); expect([...activeEnemyDisarmChannels(ascension, "actor", 101)].sort()).toEqual(["main-hand", "ranged"]); expect([...activeEnemyDisarmChannels(ascension, "actor", 5_100)]).toEqual([]); expect([...activeEnemyDisarmChannels(ascension, "other", 101)]).toEqual([]); const native = applyAura([], { definition: { id: "native-disarm", name: "Native", disposition: "debuff", durationMs: 10_000, utilityTags: ["rom-disarm", "enemy-disarm:unknown:main-hand"], modifiers: [] }, sourceId: "mob", targetId: "actor", now: 100, }).auras; expect([...activeEnemyDisarmChannels(native, "actor", 101)]).toEqual([]); }); it("projects only source item-dependent passive auras with no usable alternate weapon", () => { const itemAura = { id: "weapon-passive-aura", name: "Weapon passive", disposition: "buff" as const, durationMs: null, modifiers: [{ kind: "stat" as const, stat: "criticalStrikeRating", operation: "flat" as const, value: 20 }] }; const active = applyAura([], { definition: itemAura, sourceId: "actor", targetId: "ally", now: 100 }).auras; const otherCaster = applyAura(active, { definition: itemAura, sourceId: "other", targetId: "ally", now: 100 }).auras; const disarmed = [...otherCaster, ...disarmAuras("ascension")]; const actorTargetedAura = applyAura(disarmed, { definition: itemAura, sourceId: "other", targetId: "actor", now: 100, }).auras; const passiveRequirement = [{ auraDefinitionId: itemAura.id, equippedItemClass: 2, equippedItemSubclassMask: 1 << 15, passive: true }]; const mainHandOnly = enemyDisarmItemDependentAuraSuppressionIds( disarmed, "actor", 101, passiveRequirement, { "main-hand": weapon("dagger") }, ); expect([...mainHandOnly]).toEqual(["ally:weapon-passive-aura:actor"]); expect(mainHandOnly.has("ally:weapon-passive-aura:other")).toBe(false); expect(enemyDisarmAurasForWeaponChannel( actorTargetedAura, "actor", 101, "main-hand", passiveRequirement, { "main-hand": weapon("dagger") }, ).filter((aura) => aura.targetId === "actor" && aura.definition.id === itemAura.id)).toEqual([]); expect(enemyDisarmAurasForWeaponChannel( actorTargetedAura, "actor", 101, "off-hand", passiveRequirement, { "main-hand": weapon("dagger"), "off-hand": weapon("dagger", "off-hand") }, ).filter((aura) => aura.targetId === "actor" && aura.definition.id === itemAura.id)).toHaveLength(1); expect(enemyDisarmItemDependentAuraSuppressionIds( disarmed, "actor", 101, passiveRequirement, { "main-hand": weapon("dagger"), "off-hand": weapon("dagger", "off-hand") }, ).size).toBe(0); expect(enemyDisarmItemDependentAuraSuppressionIds( disarmed, "actor", 101, [{ ...passiveRequirement[0], passive: false }], { "main-hand": weapon("dagger") }, ).size).toBe(0); expect(enemyDisarmItemDependentAuraSuppressionIds( disarmed, "actor", 101, passiveRequirement, { "main-hand": weapon("dagger") }, true, ).size).toBe(0); expect(enemyDisarmAurasForWeaponChannel( actorTargetedAura, "actor", 101, "main-hand", passiveRequirement, { "main-hand": weapon("dagger") }, true, ).filter((aura) => aura.targetId === "actor" && aura.definition.id === itemAura.id)).toHaveLength(1); expect(enemyDisarmItemDependentAuraSuppressionIds( disarmed, "actor", 5_100, passiveRequirement, { "main-hand": weapon("dagger") }, ).size).toBe(0); }); it("derives target usability and only suppresses disarmed weapon fields in an ephemeral stats view", () => { const main = weapon("dagger"); const off = weapon("mace"); const ranged = { weaponType: "bow", inventorySlot: "ranged", stats: { ...EMPTY_ITEM_STATS, rangedWeapon: { damageMin: 8, damageMax: 12, damageSchool: "physical" as const, speedMs: 2_500 } } }; expect(enemyDisarmTargetWeaponsFromLoadout({ "main-hand": main, "off-hand": off, ranged })).toEqual({ mainHand: true, offHand: true, ranged: true, feralForm: false, }); expect(enemyDisarmTargetWeaponsFromLoadout({ "main-hand": main }, true).feralForm).toBe(true); const channels = activeEnemyDisarmChannels(disarmAuras("ascension"), "actor", 101); const adjusted = enemyDisarmAdjustedWeaponStats({ ...EMPTY_ITEM_STATS, meleeWeapon: main.stats.meleeWeapon, rangedWeapon: ranged.stats.rangedWeapon, strength: 14 }, channels); expect(adjusted).toMatchObject({ meleeWeapon: { damageMin: 1, damageMax: 2, damageSchool: "physical", speedMs: 2_000 }, rangedWeapon: null, strength: 14, }); expect(enemyDisarmAdjustedWeaponStats( { ...EMPTY_ITEM_STATS, meleeWeapon: main.stats.meleeWeapon }, channels, true, ).meleeWeapon).toEqual(main.stats.meleeWeapon); expect(EMPTY_ITEM_STATS.meleeWeapon).toBeNull(); }); it("blocks weapon-required casts only when all matching weapon slots are disabled", () => { const candidates = enemyDisarmRequirementCandidates({ "main-hand": weapon("dagger") }); const dagger = { equippedItemClass: 2, equippedItemSubclassMask: 1 << 15 }; expect(enemyDisarmInvalidatingProfile(disarmAuras("ascension"), "actor", 101, dagger, candidates)).toBe("ascension"); expect(enemyDisarmInvalidatingProfile(disarmAuras("epoch"), "actor", 101, dagger, candidates)).toBe("epoch"); const dualWieldCandidates = enemyDisarmRequirementCandidates({ "main-hand": weapon("dagger"), "off-hand": weapon("dagger"), }); expect(enemyDisarmInvalidatingProfile(disarmAuras("ascension"), "actor", 101, dagger, dualWieldCandidates)).toBeUndefined(); expect(enemyDisarmInvalidatingProfile(disarmAuras("ascension"), "actor", 101, { ...dagger, equippedItemInventoryMask: 1 << 21 }, candidates)).toBeUndefined(); expect(enemyDisarmInvalidatingProfile(disarmAuras("ascension"), "actor", 101, { equippedItemClass: -1, equippedItemSubclassMask: 0 }, candidates)).toBeUndefined(); expect(enemyDisarmInvalidatingProfile(disarmAuras("ascension"), "actor", 101, { ...dagger, equippedItemSubclassMask: 1 << 4 }, candidates)).toBeUndefined(); expect(enemyDisarmBlocksAbilityCast(disarmAuras("ascension"), "actor", 101, { ...dagger, createsAura: true, }, candidates)).toBe(false); expect(enemyDisarmBlocksAbilityCast(disarmAuras("ascension"), "actor", 101, { ...dagger, createsAura: true, attributes3: 0x00000400, }, candidates)).toBe(true); expect(enemyDisarmBlocksAbilityCast(disarmAuras("ascension"), "actor", 101, { ...dagger, createsAura: true, attributes3: 0x01000000, }, candidates)).toBe(false); expect(enemyDisarmBlocksAbilityCast(disarmAuras("ascension"), "actor", 101, { ...dagger, passive: true, }, candidates)).toBe(true); expect(enemyDisarmBlocksAbilityCast(disarmAuras("ascension"), "actor", 101, { ...dagger, attributes3: 0x00000400 | 0x01000000, }, [ ...candidates, { channel: "off-hand", itemClass: 2, subclass: 15 }, ])).toBe(true); const offHandOnlyAura = applyAura([], { definition: { id: "source-offhand-test", name: "Synthetic off-hand channel test", disposition: "debuff", durationMs: 5_000, utilityTags: ["enemy-disarm:epoch:off-hand"], modifiers: [], }, sourceId: "mob", targetId: "actor", now: 100, }).auras; expect(enemyDisarmBlocksAbilityCast(offHandOnlyAura, "actor", 101, { ...dagger, attributes3: 0x00000400 | 0x01000000, }, [ ...candidates, { channel: "off-hand", itemClass: 2, subclass: 15 }, ])).toBe(true); }); it("selects the DBC attack channel and changes only that ability's weapon contribution", () => { const sourceHeroicStrike = WOW335_ABILITIES_BY_CLASS.warrior .find((ability) => ability.name === "Heroic Strike")!.ranks![0].wowSpellDefense!; const sourceSteadyShot = WOW335_ABILITIES_BY_CLASS.hunter .find((ability) => ability.name === "Steady Shot")!.ranks![0].wowSpellDefense!; expect(enemyDisarmAbilityWeaponChannel({ ...sourceHeroicStrike, damageClass: 2 })).toBe("main-hand"); expect(enemyDisarmAbilityWeaponChannel({ ...sourceSteadyShot, damageClass: 3, equippedItemSubclassMask: 1 << 2 })).toBe("ranged"); expect(enemyDisarmAbilityWeaponChannel({ damageClass: 2, attributes3: 0x01000000 })).toBe("off-hand"); expect(enemyDisarmAbilityWeaponChannel({ damageClass: 3, attributes0: 0x00000002 })).toBe("ranged"); expect(enemyDisarmAbilityWeaponChannel({ damageClass: 3, isRangedWeaponSpell: false })).toBe("main-hand"); expect(enemyDisarmAbilityWeaponChannel({ damageClass: 1 })).toBeUndefined(); const weaponStats = weapon("dagger").stats!.meleeWeapon!; const ascensionChannels = activeEnemyDisarmChannels(disarmAuras("ascension"), "actor", 101); expect(enemyDisarmAdjustedAbilityWeapon(weaponStats, "main-hand", ascensionChannels)).toEqual({ damageMin: 1, damageMax: 2, damageSchool: "physical", speedMs: 2_000, }); expect(enemyDisarmAdjustedAbilityWeapon(weaponStats, "off-hand", ascensionChannels)).toEqual(weaponStats); expect(enemyDisarmAdjustedAbilityWeapon(weaponStats, "off-hand", new Set(["off-hand"]))).toBeNull(); expect(enemyDisarmAdjustedAbilityWeapon(weaponStats, "ranged", ascensionChannels)).toBeNull(); expect(enemyDisarmAdjustedAbilityWeapon(weaponStats, "main-hand", ascensionChannels, true)).toEqual(weaponStats); }); it("applies the creature cast gate only to the selected disarmed weapon channel", () => { const ascension = disarmAuras("ascension"); expect(enemyDisarmBlocksNpcWeaponAbility(ascension, "actor", 101, { damageClass: 2 })).toBe(true); expect(enemyDisarmBlocksNpcWeaponAbility(ascension, "actor", 101, { damageClass: 2, attributes3: 0x01000000 })).toBe(false); expect(enemyDisarmBlocksNpcWeaponAbility(ascension, "actor", 101, { damageClass: 3, attributes0: 0x00000002 })).toBe(true); expect(enemyDisarmBlocksNpcWeaponAbility(ascension, "actor", 101, { damageClass: 1 })).toBe(false); expect(enemyDisarmBlocksNpcWeaponAbility(disarmAuras("epoch"), "actor", 101, { damageClass: 3, isRangedWeaponSpell: true })).toBe(false); }); });