Files
healer-man/src/game/enemyDisarmRuntime.test.ts
T
2026-10-06 22:25:34 -04:00

224 lines
12 KiB
TypeScript

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);
});
});