146 lines
8.5 KiB
TypeScript
146 lines
8.5 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { abilitiesForClass, abilityAtLevel } from "./abilityCatalog";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { applyAura } from "./combatAuras";
|
|
import { useCombatStore, equipmentAdjustedCombatAmount } from "./combatStore";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { usePartyStore } from "./partyStore";
|
|
|
|
const ability = (name: string, level = 8) => abilityAtLevel(abilitiesForClass("primalist").find(a => a.name === name)!, level);
|
|
const gains = (id: string) => useCombatStore.getState().combatEvents.filter(e => e.kind === "resource" && e.abilityId === id);
|
|
|
|
function beginBarrage(health = 100_000, absorb = 0): void {
|
|
const combat = useCombatStore.getState();
|
|
combat.registerMob("target", { name: "Target", level: 8, maxHealth: health, xpReward: 0 });
|
|
setMobPosition("target", [25, 0, 0]);
|
|
combat.selectMob("target");
|
|
if (absorb > 0) useCombatStore.setState(state => ({ auras: applyAura(state.auras, {
|
|
definition: { id: "stone-absorb", name: "Stone Absorb", disposition: "buff", durationMs: 10_000,
|
|
absorbs: [{ id: "pool", amount: absorb }] }, sourceId: "target", targetId: "target", now: 1_000,
|
|
}).auras }));
|
|
const rageBefore = useCombatStore.getState().resource;
|
|
expect(combat.castAbility(ability("Geode Barrage").id, undefined, 1_000).ok).toBe(true);
|
|
expect(useCombatStore.getState().resource).toBe(rageBefore);
|
|
combat.tick(0, 3_000); // Finish the two-second cast.
|
|
combat.tick(0, 3_000); // Resolve its initial stone.
|
|
}
|
|
|
|
describe("Primalist rage generation", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry(); clearPartyRuntimeRegistry();
|
|
usePartyStore.setState({ members: [], selectedMemberId: null });
|
|
useCombatStore.getState().initializeCharacter({ classId: "primalist", role: "healer", level: 8 });
|
|
});
|
|
|
|
it("lands three stones over one second and grants 5 rage per hit without weapon attacks", () => {
|
|
beginBarrage();
|
|
expect(useCombatStore.getState().resource).toBe(5);
|
|
useCombatStore.getState().tick(0, 3_499);
|
|
expect(useCombatStore.getState().resource).toBe(5);
|
|
useCombatStore.getState().tick(0, 3_500);
|
|
expect(useCombatStore.getState().resource).toBe(10);
|
|
useCombatStore.getState().tick(0, 4_000);
|
|
expect(useCombatStore.getState().resource).toBe(15);
|
|
expect(useCombatStore.getState().resourcePools.rage).toBe(15);
|
|
expect(useCombatStore.getState().autoAttackTargetId).toBeNull();
|
|
expect(gains(ability("Geode Barrage").id).map(e => [e.occurredAt, e.effectiveAmount]))
|
|
.toEqual([[3_000, 5], [3_500, 5], [4_000, 5]]);
|
|
expect(useCombatStore.getState().combatEvents.filter(e => e.kind === "damage" && e.abilityId === ability("Geode Barrage").id)).toHaveLength(3);
|
|
useCombatStore.getState().tick(0, 5_000);
|
|
expect(gains(ability("Geode Barrage").id)).toHaveLength(3);
|
|
});
|
|
|
|
it("uses the ranked damage with nature spell power and attack power once per stone", () => {
|
|
const spell = ability("Geode Barrage", 80);
|
|
const stone = spell.effects.find(e => e.kind === "dot")!;
|
|
const amount = (gear: typeof EMPTY_ITEM_STATS) => equipmentAdjustedCombatAmount(stone, 80, gear, "primalist", spell);
|
|
const base = amount(EMPTY_ITEM_STATS);
|
|
expect(amount({ ...EMPTY_ITEM_STATS, spellPower: 100 }) - base).toBe(23);
|
|
expect(amount({ ...EMPTY_ITEM_STATS, attackPower: 100 }) - base).toBe(9);
|
|
expect(base).toBeGreaterThan(equipmentAdjustedCombatAmount(ability("Geode Barrage").effects.find(e => e.kind === "dot")!, 8, EMPTY_ITEM_STATS, "primalist", ability("Geode Barrage")));
|
|
});
|
|
|
|
it("stops rage gains when the target dies and caps gains at 100", () => {
|
|
beginBarrage(1);
|
|
useCombatStore.getState().tick(0, 5_000);
|
|
expect(gains(ability("Geode Barrage").id)).toHaveLength(1);
|
|
expect(useCombatStore.getState().resource).toBe(5);
|
|
useCombatStore.getState().initializeCharacter({ classId: "primalist", role: "healer", level: 8 });
|
|
useCombatStore.setState({ resource: 98, resourcePools: { ...useCombatStore.getState().resourcePools, rage: 98 } });
|
|
beginBarrage();
|
|
useCombatStore.getState().tick(0, 4_000);
|
|
expect(useCombatStore.getState().resource).toBe(100);
|
|
expect(gains(ability("Geode Barrage").id).map(e => e.effectiveAmount)).toEqual([2, 0, 0]);
|
|
});
|
|
|
|
it("credits companion Barrage hits to their caster's rage pool", () => {
|
|
usePartyStore.getState().initializeParty("primalist-source", 8, useCombatStore.getState().maxHealth);
|
|
const original = usePartyStore.getState().members[0];
|
|
usePartyStore.setState({ members: [{ ...original, classId: "primalist", resourceType: "rage", resource: 0,
|
|
maxResource: 100, resourcePools: { rage: 0 }, gearStats: EMPTY_ITEM_STATS }] });
|
|
registerPartyRuntimePosition(original.id, [0, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
useCombatStore.getState().registerMob("target", { name: "Target", maxHealth: 100_000, xpReward: 0 });
|
|
setMobPosition("target", [25, 0, 0]);
|
|
const spell = ability("Geode Barrage");
|
|
const stone = spell.effects.find(e => e.kind === "dot")!;
|
|
if (stone.kind !== "dot") throw new Error("Missing stones");
|
|
expect(useCombatStore.getState().applyTimedActorEffect(original.id, "target", spell, stone, 1_000)).toBe(true);
|
|
useCombatStore.getState().tick(0, 2_000);
|
|
expect(useCombatStore.getState().actors[original.id].resources.rage.current).toBe(15);
|
|
expect(usePartyStore.getState().members[0].resource).toBe(15);
|
|
expect(useCombatStore.getState().resource).toBe(0);
|
|
expect(gains(spell.id).every(e => e.sourceActorId === original.id && e.targetActorId === original.id)).toBe(true);
|
|
});
|
|
|
|
it("does not grant rage for fully absorbed stones", () => {
|
|
beginBarrage(100_000, 10_000);
|
|
useCombatStore.getState().tick(0, 4_000);
|
|
expect(useCombatStore.getState().resource).toBe(0);
|
|
expect(gains(ability("Geode Barrage").id)).toHaveLength(0);
|
|
});
|
|
|
|
it("normalizes the passive Protective Roar payload to 20 rage when triggered", () => {
|
|
const spell = ability("Protective Roar");
|
|
expect(spell.passive).toBe(true);
|
|
expect(spell.target).toBe("self");
|
|
expect(spell.effects.find(e => e.kind === "resource")).toMatchObject({ resource: "rage", amount: 20 });
|
|
useCombatStore.getState().registerMob("target", { name: "Target", maxHealth: 100_000, xpReward: 0 });
|
|
setMobPosition("target", [2, 0, 0]);
|
|
useCombatStore.getState().selectMob("target");
|
|
useCombatStore.setState(state => ({ auras: applyAura(state.auras, {
|
|
definition: { id: "roar-trigger", name: "Roar Trigger", disposition: "buff", durationMs: 10_000,
|
|
procs: [{ id: "roar", triggers: "cast", chance: 1, charges: 1,
|
|
action: { kind: "custom", id: "wow335-trigger-spell", data: { spellId: spell.dbcSpellId } } }] },
|
|
sourceId: PLAYER_AGGRO_ID, targetId: PLAYER_AGGRO_ID, now: 1_000,
|
|
}).auras }));
|
|
expect(useCombatStore.getState().castAbility(ability("Primal Strike").id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().resourcePools.rage).toBe(20);
|
|
expect(useCombatStore.getState().resource).toBe(20);
|
|
});
|
|
|
|
it("restores Seismic Crash's 20 rage once per cast, alongside its initial damage pulse", () => {
|
|
const spell = ability("Seismic Crash");
|
|
useCombatStore.getState().registerMob("target", { name: "Target", maxHealth: 100_000, xpReward: 0 });
|
|
setMobPosition("target", [20, 0, 0]);
|
|
useCombatStore.getState().selectMob("target");
|
|
expect(useCombatStore.getState().castAbility(spell.id, undefined, 1_000).ok).toBe(true);
|
|
useCombatStore.getState().tick(0, 2_000);
|
|
expect(gains(spell.id).map(e => e.effectiveAmount)).toEqual([20]);
|
|
expect(useCombatStore.getState().combatEvents.filter(e => e.kind === "damage" && e.abilityId === spell.id)).toHaveLength(3);
|
|
});
|
|
|
|
it("grants 4 rage when Seismic Spike lands", () => {
|
|
useCombatStore.getState().initializeCharacter({ classId: "primalist", role: "healer", level: 20 });
|
|
const spell = ability("Seismic Spike", 20);
|
|
useCombatStore.getState().registerMob("target", { name: "Target", level: 20, maxHealth: 100_000, xpReward: 0 });
|
|
setMobPosition("target", [15, 0, 0]);
|
|
useCombatStore.getState().selectMob("target");
|
|
expect(useCombatStore.getState().castAbility(spell.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().resource).toBe(4);
|
|
expect(gains(spell.id).map(e => e.effectiveAmount)).toEqual([4]);
|
|
});
|
|
});
|