import { beforeAll, describe, expect, it } from "vitest"; import { abilitiesForClass, abilityAtLevel, preloadAbilityCatalogsForClasses } from "./abilityCatalog"; import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog"; import { enemyNpcSourceSpellBlockMetadata, SPELL_ATTR0_CU_DIRECT_DAMAGE, SPELL_ATTR3_ALWAYS_HIT, SPELL_DAMAGE_CLASS_MELEE, SPELL_SCHOOL_MASK_NORMAL, } from "./enemyBlocking"; import { enemyUtilityEffectsAfterBlock, enemyUtilitySpellBlockAbilityContract, enemyUtilitySpellBlockDisposition, enemyUtilitySpellBlockSourceContract, } from "./enemyUtilitySpellBlocking"; beforeAll(async () => { await preloadAbilityCatalogsForClasses(["warrior", "rogue"]); }); const sourceSpell = enemyNpcSourceSpellBlockMetadata(72, "wow335")!; const utilityEffects = [ { kind: "interrupt", lockoutMs: 6_000 }, { kind: "apply-aura", spellId: 29_703 }, { kind: "status", status: "silence", durationMs: 6_000 }, ] as const; const baseInput = { spellId: 72, sourceProfile: "wow335" as const, spellDefense: sourceSpell, casterKind: "player" as const, targetKind: "ordinary-npc" as const, }; describe("ordinary NPC utility spell full-hit blocking", () => { it("publishes the exact Shield Bash, Kick, and Pummel rank contracts", () => { expect(enemyUtilitySpellBlockSourceContract(72, "wow335")).toMatchObject({ spellId: 72, sourceProfile: "wow335", abilityId: "wow335-warrior-shield-bash", classId: "warrior", abilityName: "Shield Bash", spellDefense: { damageClass: SPELL_DAMAGE_CLASS_MELEE, schoolMask: SPELL_SCHOOL_MASK_NORMAL, attributes0: 327696, attributes3: 8, directDamageFromEffects: false, sourceProfile: "wow335", customAttributeEvidence: { knownMask: 131_328, value: 0 }, }, dbcEffects: [ { index: 0, effectType: 68, auraType: 0, mechanic: 26, targetA: 6, targetB: 0, basePoints: 0, triggerSpellId: 0 }, { index: 1, effectType: 64, auraType: 0, mechanic: 0, targetA: 6, targetB: 0, basePoints: 0, triggerSpellId: 29_703 }, ], projectedEffects: [ { kind: "interrupt", lockoutMs: 6_000 }, { kind: "trigger-spell", spellId: 29_703 }, ], }); expect(enemyUtilitySpellBlockSourceContract(1766, "wow335")).toMatchObject({ spellId: 1766, abilityId: "rogue-kick", sourceAbilityId: "wow335-rogue-kick", classId: "rogue", abilityName: "Kick", dbcEffects: [ { index: 0, effectType: 68, auraType: 0, mechanic: 26, targetA: 6, targetB: 0, basePoints: 1, triggerSpellId: 0 }, { index: 1, effectType: 0, auraType: 0, mechanic: 0, targetA: 0, targetB: 0, basePoints: 0, triggerSpellId: 0 }, ], projectedEffects: [{ kind: "interrupt", lockoutMs: 5_000 }], }); expect(enemyUtilitySpellBlockSourceContract(6552, "wow335")).toMatchObject({ spellId: 6552, abilityId: "wow335-warrior-pummel", classId: "warrior", abilityName: "Pummel", dbcEffects: [ { index: 0, effectType: 68, auraType: 0, mechanic: 26, targetA: 6, targetB: 0, basePoints: 1, triggerSpellId: 0 }, { index: 1, effectType: 0, auraType: 0, mechanic: 0, targetA: 0, targetB: 0, basePoints: 0, triggerSpellId: 0 }, ], projectedEffects: [{ kind: "interrupt", lockoutMs: 4_000 }], }); expect(enemyUtilitySpellBlockSourceContract(72, "ascension")).toBeUndefined(); expect(enemyUtilitySpellBlockSourceContract(1_671, "wow335")).toBeUndefined(); }); it("binds blockability to the actual selected playable ability and exact projected effects", () => { const cases = [ { classId: "warrior" as const, name: "Shield Bash", spellId: 72 }, { classId: "rogue" as const, name: "Kick", spellId: 1_766 }, { classId: "warrior" as const, name: "Pummel", spellId: 6_552 }, ]; for (const item of cases) { const base = abilitiesForClass(item.classId).find((ability) => ability.name === item.name); expect(base, item.name).toBeDefined(); const selected = abilityAtLevel(base!, 80); expect(selected.dbcSpellId, item.name).toBe(item.spellId); expect(selected.source, item.name).toBe("wow335-progression"); expect(enemyUtilitySpellBlockAbilityContract(selected)?.spellId, JSON.stringify(selected)).toBe(item.spellId); } const rawKick = WOW335_ABILITIES_BY_CLASS.rogue.find((ability) => ability.id === "wow335-rogue-kick"); expect(rawKick).toBeDefined(); expect(enemyUtilitySpellBlockAbilityContract(abilityAtLevel(rawKick!, 80))?.spellId).toBe(1_766); const kick = abilityAtLevel(abilitiesForClass("rogue").find((ability) => ability.name === "Kick")!, 80); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, id: "wrong-chain" })).toBeUndefined(); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, classId: "warrior" })).toBeUndefined(); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, source: "runewaker" })).toBeUndefined(); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, dbcSpellId: 6552 })).toBeUndefined(); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, effects: [ ...kick.effects, { kind: "status", status: "stun", durationMs: 1_000 }, ] })).toBeUndefined(); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, effects: [ { kind: "interrupt", lockoutMs: 4_000 }, ] })).toBeUndefined(); expect(enemyUtilitySpellBlockAbilityContract({ ...kick, wowSpellDefense: { ...kick.wowSpellDefense!, customAttributes: SPELL_ATTR0_CU_DIRECT_DAMAGE, } })).toBeUndefined(); }); it("suppresses every utility effect after a resolved full block", () => { const disposition = enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block" }); const effectsToApply = enemyUtilityEffectsAfterBlock(utilityEffects, disposition); const sideEffects: string[] = []; for (const effect of effectsToApply) sideEffects.push(effect.kind); expect(disposition).toBe("blocked"); expect(effectsToApply).toEqual([]); expect(sideEffects).toEqual([]); }); it("keeps landed utility effects and suppresses ordinary unsuccessful hit-table outcomes", () => { expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "hit" })).toBe("apply"); expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "crit" })).toBe("apply"); for (const outcome of ["miss", "dodge", "parry", "resist"] as const) { const disposition = enemyUtilitySpellBlockDisposition({ ...baseInput, outcome }); expect(disposition).toBe("avoided"); expect(enemyUtilityEffectsAfterBlock(utilityEffects, disposition)).toEqual([]); } }); it("preserves the existing fallback for missing source, rank, profile, or signature", () => { const missingProof = { ...sourceSpell }; delete (missingProof as { customAttributeEvidence?: unknown }).customAttributeEvidence; expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", spellDefense: missingProof })).toBe("fallback"); expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", spellId: undefined })).toBe("fallback"); expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", spellId: 1_671 })).toBe("fallback"); expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", sourceProfile: "ascension" })).toBe("fallback"); expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: undefined })).toBe("fallback"); expect(enemyUtilityEffectsAfterBlock(utilityEffects, "fallback")).toBe(utilityEffects); }); it("rejects signatures whose source hit-table attributes disable or redirect the block phase", () => { for (const spellDefense of [ { ...sourceSpell, attributes3: sourceSpell.attributes3 | SPELL_ATTR3_ALWAYS_HIT }, { ...sourceSpell, customAttributes: SPELL_ATTR0_CU_DIRECT_DAMAGE }, { ...sourceSpell, damageClass: 1 }, { ...sourceSpell, schoolMask: 0x10 }, { ...sourceSpell, attributes3: 0 }, ]) { expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", spellDefense })).toBe("fallback"); } }); it("excludes NPC outbound, native, character, summon, and unknown target paths", () => { expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", casterKind: "enemy-npc" })).toBe("fallback"); expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", casterKind: "rom-native" })).toBe("fallback"); for (const targetKind of ["character", "native-rom", "summon", "unknown"] as const) { expect(enemyUtilitySpellBlockDisposition({ ...baseInput, outcome: "block", targetKind })).toBe("fallback"); } }); });