Files
healer-man/src/game/enemyControlImmunityPurge.integration.test.ts
2026-10-07 13:05:04 -04:00

116 lines
7.7 KiB
TypeScript

import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { abilitiesForClass, abilityAtLevel, abilityTalentNodeId, preloadAbilityCatalogsForClasses, type AbilityDefinition } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { applyAura } from "./combatAuras";
import { useCombatStore } from "./combatStore";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import { clearMobRuntimeRegistry } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { enemyControlImmunityPurgeProofForAbility } from "./enemyControlImmunityPurge";
beforeAll(async () => { await preloadAbilityCatalogsForClasses(["warrior", "death-knight"]); });
afterEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); });
function selected(classId: "warrior" | "death-knight", name: string): AbilityDefinition {
return abilityAtLevel(abilitiesForClass(classId).find((ability) => ability.name === name)!, 80);
}
function prepare(recipient: "player" | "party", mechanic: "fear" | "stun", spellId: 19134 | 20511 = 19134) {
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80 });
const member = createConfiguredPartyMember({ name: "Synthetic Protection Warrior", classId: "warrior",
specialization: "Protection", specializationId: "warrior:protection", talentTreeId: "warrior-protection",
talentRanks: {}, role: "tank" }, 80, 0, "purge-fixture");
usePartyStore.setState({ members: [{ ...member, controlledUntil: 9_000, controlMechanic: mechanic }] });
const id = recipient === "player" ? PLAYER_AGGRO_ID : member.id;
useCombatStore.getState().synchronizeActors();
useCombatStore.setState((state) => ({ controlledUntil: recipient === "player" ? 9_000 : 0,
controlMechanic: recipient === "player" ? mechanic : null,
auras: [...applyAura(state.auras, { definition: {
id: `enemy-control:source-spell-${spellId}`, name: "Synthetic pinned shout aura",
disposition: "debuff", durationMs: 8_000, utilityTags: [`enemy-control-source:${mechanic}`],
modifiers: [{ kind: "damage", direction: "taken", school: "physical", operation: "percent", value: 0.15 }],
}, sourceId: "synthetic-source-caster", targetId: id, now: 1_000 }).auras] }));
return id;
}
function grant(id: string, ability: AbilityDefinition, auraType: number) {
const effect = ability.effects.find((entry) => entry.kind === "apply-aura" && entry.sourceAuraType === auraType);
if (!effect || effect.kind !== "apply-aura") throw new Error("Missing actual selected immunity grant");
// An accepted grant fixture exercises the real shared player/party consumer.
return useCombatStore.getState().applyActorAura(id, id, ability, effect, 2_000);
}
describe("source immunity grants through actual runtime consumers", () => {
it("casts actual Berserker Rage while feared and removes the source aura permanently", () => {
prepare("player", "fear");
const ability = selected("warrior", "Berserker Rage");
expect(enemyControlImmunityPurgeProofForAbility(ability)).toBeDefined();
expect(useCombatStore.getState().castAbility(ability.id, { now: 2_000 }).ok).toBe(true);
expect(useCombatStore.getState().controlledUntil).toBe(0);
expect(useCombatStore.getState().controlMechanic).toBeNull();
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "enemy-control:source-spell-19134")).toBe(false);
useCombatStore.getState().tick(0.1, 2_100);
expect(useCombatStore.getState().controlledUntil).toBe(0);
});
it("escapes the source FEAR-mechanic child stun through an actual Berserker Rage cast", () => {
prepare("player", "stun", 20511);
expect(useCombatStore.getState().castAbility(selected("warrior", "Berserker Rage").id, { now: 2_000 }).ok).toBe(true);
expect(useCombatStore.getState().controlMechanic).toBeNull();
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "enemy-control:source-spell-20511")).toBe(false);
});
it("blocks Berserker Rage when a separate generic stun overlaps the longer source fear", () => {
prepare("player", "fear");
useCombatStore.setState((state) => ({ auras: [...applyAura(state.auras, { definition: {
id: "enemy-control:another-caster:another-spell:stun", name: "Shorter generic stun",
disposition: "debuff", durationMs: 3_000,
}, sourceId: "another-caster", targetId: PLAYER_AGGRO_ID, now: 1_000 }).auras] }));
expect(useCombatStore.getState().castAbility(selected("warrior", "Berserker Rage").id, { now: 2_000 }))
.toMatchObject({ ok: false, reason: "player-controlled" });
});
it.each([
["fear", 19134],
["stun", 20511],
] as const)("rejects learned Bladestorm while under source %s without spending resources or purging", (mechanic, spellId) => {
prepare("player", mechanic, spellId);
const ability = selected("warrior", "Bladestorm");
const nodeId = abilityTalentNodeId(ability);
expect(nodeId).not.toBeNull();
// Synthetic learned rank isolates the cast-control gate from talent-tree allocation.
useCombatStore.setState({ talentRanks: { [nodeId!]: 1 } });
const before = useCombatStore.getState();
expect(useCombatStore.getState().castAbility(ability.id, { now: 2_000 }))
.toMatchObject({ ok: false, reason: "player-controlled" });
const after = useCombatStore.getState();
expect(after.auras).toEqual(before.auras);
expect(after.controlledUntil).toBe(before.controlledUntil);
expect(after.controlMechanic).toBe(mechanic);
expect(after.resourcePools).toEqual(before.resourcePools);
expect(after.cooldowns).toEqual(before.cooldowns);
expect(after.globalCooldownEndsAt).toBe(before.globalCooldownEndsAt);
});
it.each(["player", "party"] as const)("purges the whole FEAR-mechanic child and its rider for %s on an accepted Bladestorm grant", (recipient) => {
const id = prepare(recipient, "stun", 20511);
const ability = selected("warrior", "Bladestorm");
expect(enemyControlImmunityPurgeProofForAbility(ability)).toBeDefined();
expect(grant(id, ability, 147)).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === id
&& aura.definition.id === "enemy-control:source-spell-20511")).toBe(false);
expect(recipient === "player" ? useCombatStore.getState().controlledUntil
: usePartyStore.getState().members[0]!.controlledUntil).toBe(0);
expect(useCombatStore.getState().auras.find((aura) => aura.targetId === id
&& aura.definition.controlImmunities?.includes("fear"))?.definition.controlImmunities).toContain("stun");
});
it.each(["player", "party"] as const)("preserves source stun and its rider under non-purging Icebound Fortitude for %s", (recipient) => {
const id = prepare(recipient, "stun", 20511);
expect(grant(id, selected("death-knight", "Icebound Fortitude"), 77)).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === id
&& aura.definition.id === "enemy-control:source-spell-20511")).toBe(true);
expect(recipient === "player" ? useCombatStore.getState().controlledUntil
: usePartyStore.getState().members[0]!.controlledUntil).toBe(9_000);
});
it("retains a separate generic stun after a bounded source shout purge", () => {
const id = prepare("party", "fear");
useCombatStore.setState((state) => ({ auras: [...applyAura(state.auras, { definition: {
id: "enemy-control:synthetic-other:cast:stun", name: "Separate generic stun", disposition: "debuff", durationMs: 5_000,
}, sourceId: "another-caster", targetId: id, now: 1_000 }).auras] }));
expect(grant(id, selected("warrior", "Berserker Rage"), 77)).toBe(true);
expect(usePartyStore.getState().members[0]).toMatchObject({ controlledUntil: 6_000, controlMechanic: "stun" });
});
});