import { describe, expect, it } from "vitest"; import type { ClassId } from "../app/characterCatalog"; import { coaSpellById, coaTreeEntryById } from "./coaLiveCatalog"; import { applyAura, resolveDamageValue, type ActiveAura } from "./combatAuras"; import { applyCoaGuardianVanguardEvent, COA_GUARDIAN_VANGUARD_AURA, COA_GUARDIAN_VANGUARD_AURA_ID, COA_GUARDIAN_VANGUARD_DURATION_MS, COA_GUARDIAN_VANGUARD_MAX_STACKS, COA_GUARDIAN_VANGUARDS_SHIELD_TALENT_ENTRY_ID, hasCoaGuardianVanguardsShield, type CoaGuardianVanguardActor, } from "./coaGuardianVanguardRuntime"; const actor = ( id = "guardian-player", classId: ClassId = "guardian", rank = 1, ): CoaGuardianVanguardActor => ({ id, classId, talentRanks: { [`coa-entry-${COA_GUARDIAN_VANGUARDS_SHIELD_TALENT_ENTRY_ID}`]: rank }, }); describe("CoA Guardian Vanguard runtime", () => { it("matches the August node and global DBC stack payload", () => { expect(coaTreeEntryById(COA_GUARDIAN_VANGUARDS_SHIELD_TALENT_ENTRY_ID)).toMatchObject({ classId: 18, name: "Vanguard's Shield", entryType: "Talent", rankDescriptions: [{ rank: 1, spellId: 804383 }], }); expect(coaSpellById(804383)?.dbc?.effects.some((effect) => effect.triggerSpellId === 804384)).toBe(true); expect(coaSpellById(804384)?.dbc).toMatchObject({ durationMs: COA_GUARDIAN_VANGUARD_DURATION_MS, stackAmount: COA_GUARDIAN_VANGUARD_MAX_STACKS, effects: [expect.objectContaining({ auraType: 87, basePoints: -2, miscValue: 126, })], }); expect(COA_GUARDIAN_VANGUARD_AURA.modifiers).toHaveLength(6); }); it("gates stack transitions on the learned Guardian talent", () => { expect(hasCoaGuardianVanguardsShield(actor())).toBe(true); expect(hasCoaGuardianVanguardsShield(actor("other", "guardian", 0))).toBe(false); expect(hasCoaGuardianVanguardsShield(actor("other", "warrior", 1))).toBe(false); const current: readonly ActiveAura[] = []; expect(applyCoaGuardianVanguardEvent(current, actor("other", "guardian", 0), "block", 100)) .toBe(current); }); it("adds one stack on each landed auto attack or block, refreshes for a minute, and caps at ten", () => { let auras: readonly ActiveAura[] = []; auras = applyCoaGuardianVanguardEvent(auras, actor(), "auto-attack-landed", 1_000); expect(auras[0]).toMatchObject({ definition: { id: COA_GUARDIAN_VANGUARD_AURA_ID }, sourceId: "guardian-player", targetId: "guardian-player", stacks: 1, appliedAt: 1_000, expiresAt: 1_000 + COA_GUARDIAN_VANGUARD_DURATION_MS, }); auras = applyCoaGuardianVanguardEvent(auras, actor(), "block", 2_000); expect(auras[0]).toMatchObject({ stacks: 2, appliedAt: 2_000, expiresAt: 62_000 }); for (let index = 2; index < 12; index += 1) { auras = applyCoaGuardianVanguardEvent(auras, actor(), "block", 2_000 + index * 100); } expect(auras[0]!.stacks).toBe(COA_GUARDIAN_VANGUARD_MAX_STACKS); }); it("reduces each magic school by two percent per stack, excluding physical damage", () => { let tenStacks: readonly ActiveAura[] = []; for (let index = 0; index < 10; index += 1) { tenStacks = applyCoaGuardianVanguardEvent( tenStacks, actor(), index === 0 ? "block" : "auto-attack-landed", 1_000 + index, ); } for (const school of ["holy", "fire", "nature", "frost", "shadow", "arcane"] as const) { expect(resolveDamageValue(100, tenStacks, { direction: "taken", school, attackKind: "spell" })).toBe(80); } expect(resolveDamageValue(100, tenStacks, { direction: "taken", school: "physical", attackKind: "melee" })) .toBe(100); }); it("consumes one stack on a spell hit without refreshing duration and removes the last stack", () => { let auras = applyCoaGuardianVanguardEvent([], actor(), "block", 1_000); auras = applyCoaGuardianVanguardEvent(auras, actor(), "auto-attack-landed", 2_000); auras = applyCoaGuardianVanguardEvent(auras, actor(), "spell-hit", 3_000); expect(auras[0]).toMatchObject({ stacks: 1, appliedAt: 2_000, expiresAt: 62_000 }); const depleted = applyCoaGuardianVanguardEvent(auras, actor(), "spell-hit", 4_000); expect(depleted).toEqual([]); expect(applyCoaGuardianVanguardEvent(depleted, actor(), "spell-hit", 5_000)).toEqual([]); }); it("keeps stacks actor-owned and ignores unrelated event types", () => { const guardianA = actor("guardian-a"); const guardianB = actor("guardian-b"); const oneStack = applyCoaGuardianVanguardEvent([], guardianA, "block", 1_000); expect(applyCoaGuardianVanguardEvent(oneStack, guardianB, "other", 2_000)).toBe(oneStack); const both = applyCoaGuardianVanguardEvent(oneStack, guardianB, "auto-attack-landed", 2_000); expect(both.map((aura) => [aura.targetId, aura.stacks])).toEqual([ ["guardian-a", 1], ["guardian-b", 1], ]); }); it("does not stack or consume another source's aura on the same target", () => { const foreign = applyAura([], { definition: COA_GUARDIAN_VANGUARD_AURA, sourceId: "another-actor", targetId: "guardian-player", now: 1_000, stacks: 3, }).aura; const stacked = applyCoaGuardianVanguardEvent([foreign], actor(), "block", 2_000); expect(stacked).toHaveLength(2); expect(stacked.find((aura) => aura.sourceId === "another-actor")?.stacks).toBe(3); const hit = applyCoaGuardianVanguardEvent([foreign], actor(), "spell-hit", 3_000); expect(hit).toEqual([foreign]); }); it("discards an expired Vanguard aura before applying a fresh event", () => { const expired = applyCoaGuardianVanguardEvent([], actor(), "block", 1_000); const fresh = applyCoaGuardianVanguardEvent(expired, actor(), "block", 61_000); expect(fresh).toHaveLength(1); expect(fresh[0]).toMatchObject({ stacks: 1, appliedAt: 61_000, expiresAt: 121_000 }); }); });