Release Healer Man 0.1.41
This commit is contained in:
1 parent
197a64117c
commit
c3f8a69b51
42 files changed
+886
-127
No files matched your search
@@ -0,0 +1,145 @@
|
||||
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]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user