172 lines
5.8 KiB
TypeScript
172 lines
5.8 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
|
|
import { usePartyStore, createConfiguredPartyMember } from "./partyStore";
|
|
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
|
import {
|
|
WOW335_SHAMAN_FLURRY_AURA_TAG,
|
|
WOW335_SHAMAN_FLURRY_DURATION_MS,
|
|
} from "./wow335ShamanFlurryRuntime";
|
|
|
|
const TARGET_ID = "wow335-shaman-flurry-target";
|
|
const FLURRY_RANK_ID = "talent-602";
|
|
|
|
function registerTarget(): void {
|
|
useCombatStore.getState().registerMob(TARGET_ID, {
|
|
name: "Flurry target",
|
|
level: 80,
|
|
maxHealth: 1_000_000,
|
|
xpReward: 0,
|
|
});
|
|
setMobPosition(TARGET_ID, [3, 0, 0]);
|
|
useCombatStore.setState((state) => ({
|
|
mobs: {
|
|
...state.mobs,
|
|
[TARGET_ID]: {
|
|
...state.mobs[TARGET_ID]!,
|
|
characterDefender: { level: 80, dodgePercent: 0, parryPercent: 0, blockPercent: 0 },
|
|
},
|
|
},
|
|
}));
|
|
}
|
|
|
|
function setupPlayerShaman(rank: number): void {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({ classId: "shaman", level: 80, xp: 0 });
|
|
useCombatStore.getState().resetSettings();
|
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
|
usePartyStore.getState().initializeParty("wow-shaman-flurry-player", 80, useCombatStore.getState().health);
|
|
registerTarget();
|
|
useCombatStore.setState((state) => ({
|
|
gearStats: {
|
|
...state.gearStats,
|
|
hitRating: 100_000,
|
|
criticalStrikeRating: 100_000,
|
|
expertiseRating: 100_000,
|
|
},
|
|
talentRanks: { ...state.talentRanks, [FLURRY_RANK_ID]: rank },
|
|
autoAttackTargetId: TARGET_ID,
|
|
nextAutoAttackAt: 1_000,
|
|
}));
|
|
}
|
|
|
|
function flurryAura(actorId: string) {
|
|
return useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === actorId
|
|
&& aura.targetId === actorId
|
|
&& aura.definition.utilityTags?.includes(WOW335_SHAMAN_FLURRY_AURA_TAG)
|
|
));
|
|
}
|
|
|
|
describe("WoW Shaman Flurry combat integration", () => {
|
|
beforeEach(() => setupPlayerShaman(0));
|
|
|
|
it("procs from the player's landed melee crit and hastens the next main-hand interval", () => {
|
|
const intervalForRank = (rank: number) => {
|
|
setupPlayerShaman(rank);
|
|
useCombatStore.getState().tick(0.1, 1_000);
|
|
const state = useCombatStore.getState();
|
|
expect(state.lastCombatResult?.critical).toBe(true);
|
|
expect(state.lastCombatResult?.landed).toBe(true);
|
|
return state.nextAutoAttackAt - 1_000;
|
|
};
|
|
|
|
const baselineInterval = intervalForRank(0);
|
|
const flurryInterval = intervalForRank(1);
|
|
const aura = flurryAura(PLAYER_AURA_ENTITY_ID);
|
|
expect(flurryInterval).toBeLessThan(baselineInterval);
|
|
expect(aura).toMatchObject({
|
|
stacks: 3,
|
|
appliedAt: 1_000,
|
|
expiresAt: 1_000 + WOW335_SHAMAN_FLURRY_DURATION_MS,
|
|
definition: {
|
|
modifiers: [{ kind: "stat", stat: "melee-haste", operation: "flat", value: 4, perStack: false }],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("uses a generated companion Shaman's exact rank and consumes the next three autoattack attempts", () => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80, xp: 0 });
|
|
usePartyStore.getState().initializeParty("wow-shaman-flurry-companion", 80, useCombatStore.getState().health);
|
|
const baseMember = createConfiguredPartyMember({
|
|
name: "Flurry Shaman",
|
|
classId: "shaman",
|
|
specialization: "Enhancement",
|
|
specializationId: "shaman:enhancement",
|
|
talentTreeId: "shaman-enhancement",
|
|
talentRanks: { [FLURRY_RANK_ID]: 2 },
|
|
role: "damage",
|
|
}, 80, 0, "wow-shaman-flurry-companion:member");
|
|
const member = {
|
|
...baseMember,
|
|
gearStats: { ...EMPTY_ITEM_STATS, hitRating: 100_000, criticalStrikeRating: 100_000, expertiseRating: 100_000 },
|
|
health: baseMember.maxHealth,
|
|
};
|
|
usePartyStore.setState({ members: [member] });
|
|
useCombatStore.getState().commitCompanionActors([member]);
|
|
registerTarget();
|
|
|
|
const trigger = useCombatStore.getState().damageMob(
|
|
TARGET_ID,
|
|
100,
|
|
2_000,
|
|
{ actorId: member.id, role: member.role, abilityId: "party-auto-attack" },
|
|
"physical",
|
|
{
|
|
attackKind: "melee",
|
|
seed: "flurry-companion-trigger",
|
|
abilityId: "party-auto-attack",
|
|
rules: {
|
|
isAutoAttack: true,
|
|
canMiss: false,
|
|
canDodge: false,
|
|
canParry: false,
|
|
canBlock: false,
|
|
},
|
|
},
|
|
);
|
|
expect(trigger.combatResult?.landed).toBe(true);
|
|
expect(trigger.combatResult?.critical).toBe(true);
|
|
expect(flurryAura(member.id)).toMatchObject({
|
|
stacks: 3,
|
|
definition: {
|
|
modifiers: [{ kind: "stat", stat: "melee-haste", operation: "flat", value: 6, perStack: false }],
|
|
},
|
|
});
|
|
// A fake rank on the Mage player must not supply the companion's Flurry value.
|
|
useCombatStore.setState((state) => ({
|
|
talentRanks: { ...state.talentRanks, [FLURRY_RANK_ID]: 3 },
|
|
}));
|
|
|
|
for (let swing = 1; swing <= 3; swing += 1) {
|
|
useCombatStore.getState().damageMob(
|
|
TARGET_ID,
|
|
100,
|
|
2_000 + swing * 100,
|
|
{ actorId: member.id, role: member.role, abilityId: "party-auto-attack" },
|
|
"physical",
|
|
{
|
|
attackKind: "melee",
|
|
seed: `flurry-companion-swing-${swing}`,
|
|
abilityId: "party-auto-attack",
|
|
rules: {
|
|
isAutoAttack: true,
|
|
canCrit: false,
|
|
canMiss: false,
|
|
canDodge: false,
|
|
canParry: false,
|
|
canBlock: false,
|
|
},
|
|
},
|
|
);
|
|
const aura = flurryAura(member.id);
|
|
expect(aura?.stacks ?? 0).toBe(3 - swing);
|
|
}
|
|
expect(flurryAura(PLAYER_AURA_ENTITY_ID)).toBeUndefined();
|
|
});
|
|
});
|