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