Files
healer-man/src/game/coaGuardianVanguardRuntime.test.ts
T
2026-10-06 22:25:34 -04:00

142 lines
5.8 KiB
TypeScript

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