forked from phenom/healer-man
224 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|