142 lines
5.8 KiB
TypeScript
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 });
|
|
});
|
|
});
|