302 lines
13 KiB
TypeScript
302 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { abilityAtLevel } from "./abilityCatalog";
|
|
import type { ActiveAura } from "./combatAuras";
|
|
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
|
import {
|
|
enemyControlAbilityCanEscapeAuras,
|
|
enemyControlAbilityUsableWhileControlled,
|
|
enemyControlImmunityPurgeProofForAbility,
|
|
purgeNamedEnemyControlShoutAuras,
|
|
} from "./enemyControlImmunityPurge";
|
|
|
|
function selectedAbility(
|
|
spellId: number,
|
|
classId: keyof typeof WOW335_ABILITIES_BY_CLASS = "warrior",
|
|
) {
|
|
const base = WOW335_ABILITIES_BY_CLASS[classId].find((ability) => ability.dbcSpellId === spellId);
|
|
if (!base) throw new Error(`Missing generated ${classId} ability ${spellId}`);
|
|
return abilityAtLevel(base, 80);
|
|
}
|
|
|
|
function activeAura(id: string, targetId: string, expiresAt: number | null = 500): ActiveAura {
|
|
return {
|
|
instanceId: `${targetId}:${id}:enemy-source`,
|
|
definition: { id, name: id, disposition: "debuff", durationMs: expiresAt },
|
|
sourceId: "enemy-source",
|
|
targetId,
|
|
appliedAt: 0,
|
|
expiresAt,
|
|
stacks: 1,
|
|
absorbs: [],
|
|
procStates: [],
|
|
};
|
|
}
|
|
|
|
describe("source-proven enemy control immunity purge", () => {
|
|
it("proves Berserker Rage fear purge and its fear-only controlled cast exception from the selected row", () => {
|
|
const berserkerRage = selectedAbility(18499);
|
|
const proof = enemyControlImmunityPurgeProofForAbility(berserkerRage);
|
|
|
|
expect(proof).toMatchObject({
|
|
sourceProfile: "wow335",
|
|
sourceSpellId: 18499,
|
|
grantsMechanics: ["fear"],
|
|
purgesNamedShoutSpellMechanics: ["fear"],
|
|
usableWhileControlledBy: ["fear"],
|
|
directCastBypasses: [],
|
|
});
|
|
expect(enemyControlAbilityUsableWhileControlled(berserkerRage, "fear")).toBe(true);
|
|
expect(enemyControlAbilityUsableWhileControlled(berserkerRage, "stun")).toBe(false);
|
|
});
|
|
|
|
it("proves Bladestorm fear and stun grants, but no controlled-casting bypass", () => {
|
|
const bladestorm = selectedAbility(46924);
|
|
const proof = enemyControlImmunityPurgeProofForAbility(bladestorm);
|
|
|
|
expect(proof).toMatchObject({
|
|
sourceProfile: "wow335",
|
|
sourceSpellId: 46924,
|
|
grantsMechanics: ["fear", "stun"],
|
|
purgesNamedShoutSpellMechanics: ["fear"],
|
|
usableWhileControlledBy: [],
|
|
directCastBypasses: [],
|
|
});
|
|
expect(enemyControlAbilityUsableWhileControlled(bladestorm, "fear")).toBe(false);
|
|
expect(enemyControlAbilityUsableWhileControlled(bladestorm, "stun")).toBe(false);
|
|
});
|
|
|
|
it("proves Lichborne's exact Charm/Fear/Sleep purge and its separate cast-state flags", () => {
|
|
const lichborne = selectedAbility(49039, "death-knight");
|
|
const proof = enemyControlImmunityPurgeProofForAbility(lichborne);
|
|
|
|
expect(lichborne).toMatchObject({
|
|
id: "wow335-death-knight-lichborne",
|
|
name: "Lichborne",
|
|
classId: "death-knight",
|
|
source: "wow335-progression",
|
|
dbcSpellId: 49039,
|
|
wowSpellDefense: {
|
|
sourceProfile: "wow335",
|
|
damageClass: 0,
|
|
schoolMask: 32,
|
|
attributes0: 262144,
|
|
attributes1: 163872,
|
|
attributes3: 0,
|
|
attributes5: 393224,
|
|
directDamageFromEffects: false,
|
|
},
|
|
});
|
|
expect(lichborne.effects).toHaveLength(3);
|
|
const expectedEffects = [
|
|
{ miscValue: 1, sourceBasePoints: 0, sourceDieSides: 1, immunity: undefined },
|
|
{ miscValue: 5, sourceBasePoints: 0, sourceDieSides: 0, immunity: "fear" },
|
|
{ miscValue: 10, sourceBasePoints: 0, sourceDieSides: 0, immunity: "sleep" },
|
|
] as const;
|
|
lichborne.effects.forEach((effect, index) => {
|
|
const expected = expectedEffects[index];
|
|
expect(effect.kind).toBe("apply-aura");
|
|
if (effect.kind !== "apply-aura") throw new Error("Expected selected Lichborne immunity aura");
|
|
expect(effect).toMatchObject({
|
|
sourceEffectType: 6,
|
|
sourceAuraType: 77,
|
|
sourceBasePoints: expected.sourceBasePoints,
|
|
sourceDieSides: expected.sourceDieSides,
|
|
sourcePointsPerLevel: 0,
|
|
miscValue: expected.miscValue,
|
|
recipient: "caster",
|
|
});
|
|
if (expected.immunity) expect(effect.aura.controlImmunities).toContain(expected.immunity);
|
|
expect(effect.aura.id).toBe(`wow335:49039:effect:${index}:aura:77`);
|
|
});
|
|
expect(proof).toMatchObject({
|
|
sourceProfile: "wow335",
|
|
sourceSpellId: 49039,
|
|
grantsMechanics: ["charm", "fear", "sleep"],
|
|
purgesNamedShoutSpellMechanics: ["fear"],
|
|
usableWhileControlledBy: ["fear", "stun", "confuse"],
|
|
directCastBypasses: ["fear", "stun", "confuse"],
|
|
});
|
|
expect(enemyControlAbilityUsableWhileControlled(lichborne, "fear")).toBe(true);
|
|
expect(enemyControlAbilityUsableWhileControlled(lichborne, "stun")).toBe(true);
|
|
expect(enemyControlAbilityUsableWhileControlled(lichborne, "confuse")).toBe(true);
|
|
expect(enemyControlAbilityUsableWhileControlled(lichborne, "charm")).toBe(false);
|
|
expect(enemyControlAbilityUsableWhileControlled(lichborne, "sleep")).toBe(false);
|
|
});
|
|
|
|
it("fails closed for a changed profile, missing or changed raw flags, and changed aura scalars", () => {
|
|
const berserkerRage = selectedAbility(18499);
|
|
const defense = berserkerRage.wowSpellDefense!;
|
|
const fearAuraIndex = berserkerRage.effects.findIndex((effect) => (
|
|
effect.kind === "apply-aura" && effect.sourceAuraType === 77 && effect.miscValue === 5
|
|
));
|
|
const fearAura = berserkerRage.effects[fearAuraIndex];
|
|
if (fearAura.kind !== "apply-aura") throw new Error("Expected source fear immunity aura");
|
|
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...berserkerRage,
|
|
wowSpellDefense: { ...defense, sourceProfile: "ascension" },
|
|
})).toBeUndefined();
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...berserkerRage,
|
|
wowSpellDefense: { ...defense, attributes1: undefined },
|
|
})).toBeUndefined();
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...berserkerRage,
|
|
wowSpellDefense: { ...defense, attributes1: defense.attributes1! ^ 0x00008000 },
|
|
})).toBeUndefined();
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...berserkerRage,
|
|
effects: berserkerRage.effects.map((effect, index) => index === fearAuraIndex
|
|
? { ...fearAura, sourceBasePoints: 0 }
|
|
: effect),
|
|
})).toBeUndefined();
|
|
});
|
|
|
|
it("fails closed for Lichborne when its selected WoW profile, full flags, or effect scalars change", () => {
|
|
const lichborne = selectedAbility(49039, "death-knight");
|
|
const defense = lichborne.wowSpellDefense!;
|
|
const fearIndex = lichborne.effects.findIndex((effect) => (
|
|
effect.kind === "apply-aura" && effect.sourceAuraType === 77 && effect.miscValue === 5
|
|
));
|
|
const fearAura = lichborne.effects[fearIndex];
|
|
if (fearAura.kind !== "apply-aura") throw new Error("Expected selected Lichborne fear immunity aura");
|
|
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...lichborne,
|
|
wowSpellDefense: { ...defense, sourceProfile: "ascension" },
|
|
})).toBeUndefined();
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...lichborne,
|
|
wowSpellDefense: { ...defense, attributes1: undefined },
|
|
})).toBeUndefined();
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...lichborne,
|
|
wowSpellDefense: { ...defense, attributes5: defense.attributes5! ^ 0x00020000 },
|
|
})).toBeUndefined();
|
|
expect(enemyControlImmunityPurgeProofForAbility({
|
|
...lichborne,
|
|
effects: lichborne.effects.map((effect, index) => index === fearIndex
|
|
? { ...fearAura, sourceDieSides: 1 }
|
|
: effect),
|
|
})).toBeUndefined();
|
|
});
|
|
|
|
it("does not infer active purge from Icebound Fortitude's stun-immunity row", () => {
|
|
const base = WOW335_ABILITIES_BY_CLASS["death-knight"].find((ability) => ability.dbcSpellId === 48792);
|
|
if (!base) throw new Error("Missing generated Death Knight ability 48792");
|
|
const iceboundFortitude = abilityAtLevel(base, 80);
|
|
expect(iceboundFortitude.wowSpellDefense?.attributes1).toBe(0);
|
|
expect(enemyControlImmunityPurgeProofForAbility(iceboundFortitude)).toBeUndefined();
|
|
});
|
|
|
|
it("removes only active named shout auras on the proven target and retains unrelated state", () => {
|
|
const target = "party-warrior";
|
|
const proof = enemyControlImmunityPurgeProofForAbility(selectedAbility(18499));
|
|
const auras = [
|
|
activeAura("enemy-control:source-spell-5246", target),
|
|
activeAura("enemy-control:source-spell-19134", target),
|
|
activeAura("enemy-control:source-spell-20511", target),
|
|
activeAura("enemy-control:source-spell-29544", target),
|
|
activeAura("enemy-control:other-control", target),
|
|
activeAura("enemy-control:source-spell-5246", "another-party-member"),
|
|
activeAura("enemy-control:source-spell-20511", target, 100),
|
|
];
|
|
|
|
const result = purgeNamedEnemyControlShoutAuras(auras, target, proof, 100);
|
|
expect(result.removed.map((aura) => aura.definition.id)).toEqual([
|
|
"enemy-control:source-spell-5246",
|
|
"enemy-control:source-spell-19134",
|
|
"enemy-control:source-spell-20511",
|
|
"enemy-control:source-spell-29544",
|
|
]);
|
|
expect(result.auras).toEqual([auras[4], auras[5], auras[6]]);
|
|
});
|
|
|
|
it("does not accept a structurally forged proof", () => {
|
|
const aura = activeAura("enemy-control:source-spell-5246", "party-warrior");
|
|
const forged = {
|
|
sourceProfile: "wow335",
|
|
sourceSpellId: 18499,
|
|
grantsMechanics: ["fear"],
|
|
purgesNamedShoutSpellMechanics: ["fear"],
|
|
usableWhileControlledBy: ["fear"],
|
|
directCastBypasses: [],
|
|
} as const;
|
|
|
|
expect(purgeNamedEnemyControlShoutAuras([aura], "party-warrior", forged, 0)).toEqual({
|
|
auras: [aura],
|
|
removed: [],
|
|
});
|
|
});
|
|
|
|
it("allows Berserker Rage through the sourced 20511 stun only when every action-blocking control is covered", () => {
|
|
const berserkerRage = selectedAbility(18499);
|
|
const sourceChildStun = activeAura("enemy-control:source-spell-20511", "party-warrior");
|
|
const genericStun = activeAura("enemy-control:another-caster:another-spell:stun", "party-warrior");
|
|
const sourceFear = activeAura("enemy-control:source-spell-5246", "party-warrior");
|
|
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
berserkerRage, "stun", [sourceChildStun], "party-warrior", 100,
|
|
)).toBe(true);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
berserkerRage, "stun", [sourceChildStun, genericStun], "party-warrior", 100,
|
|
)).toBe(false);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
berserkerRage, "fear", [sourceFear, genericStun], "party-warrior", 100,
|
|
)).toBe(false);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
berserkerRage, "stun", [sourceFear], "party-warrior", 100,
|
|
)).toBe(false);
|
|
});
|
|
|
|
it("does not let Bladestorm borrow its purge grant as a control cast bypass", () => {
|
|
const bladestorm = selectedAbility(46924);
|
|
const sourceFear = activeAura("enemy-control:source-spell-5246", "party-warrior");
|
|
const sourceChildStun = activeAura("enemy-control:source-spell-20511", "party-warrior");
|
|
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
bladestorm, "fear", [sourceFear], "party-warrior", 100,
|
|
)).toBe(false);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
bladestorm, "stun", [sourceChildStun], "party-warrior", 100,
|
|
)).toBe(false);
|
|
});
|
|
|
|
it("allows Lichborne through sourced or directly supported control and retains unrelated auras", () => {
|
|
const lichborne = selectedAbility(49039, "death-knight");
|
|
const proof = enemyControlImmunityPurgeProofForAbility(lichborne);
|
|
const sourceChildStun = activeAura("enemy-control:source-spell-20511", "party-death-knight");
|
|
const genericStun = activeAura("enemy-control:another-caster:another-spell:stun", "party-death-knight");
|
|
const genericConfuse = activeAura("enemy-control:another-caster:another-spell:confuse", "party-death-knight");
|
|
const genericFear = activeAura("enemy-control:another-caster:another-spell:fear", "party-death-knight");
|
|
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
lichborne, "stun", [sourceChildStun], "party-death-knight", 100,
|
|
)).toBe(true);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
lichborne, "stun", [genericStun], "party-death-knight", 100,
|
|
)).toBe(true);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
lichborne, "confuse", [genericConfuse], "party-death-knight", 100,
|
|
)).toBe(true);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
lichborne, "fear", [genericFear], "party-death-knight", 100,
|
|
)).toBe(true);
|
|
expect(enemyControlAbilityCanEscapeAuras(
|
|
selectedAbility(18499), "fear", [genericFear], "party-death-knight", 100,
|
|
)).toBe(false);
|
|
const mixedFear = purgeNamedEnemyControlShoutAuras(
|
|
[genericFear, activeAura("enemy-control:source-spell-19134", "party-death-knight")],
|
|
"party-death-knight", proof, 100,
|
|
);
|
|
expect(mixedFear.removed.map((aura) => aura.definition.id)).toEqual(["enemy-control:source-spell-19134"]);
|
|
expect(mixedFear.auras).toEqual([genericFear]);
|
|
expect(purgeNamedEnemyControlShoutAuras(
|
|
[genericStun, genericConfuse], "party-death-knight", proof, 100,
|
|
)).toEqual({
|
|
auras: [genericStun, genericConfuse],
|
|
removed: [],
|
|
});
|
|
});
|
|
});
|