524 lines
23 KiB
TypeScript
524 lines
23 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { abilityAtLevel, abilitiesForClass, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
|
import { useCombatStore } from "./combatStore";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import {
|
|
clearPartyRuntimeRegistry,
|
|
registerPartyRuntimePosition,
|
|
updatePartyRuntimeTargetVisibility,
|
|
} from "./partyRuntimeRegistry";
|
|
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
|
import { createConfiguredPartyMember, usePartyStore, type PartyMember } from "./partyStore";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { partyAbilityAmount, partyAbilityCosts } from "./partyAbilityAi";
|
|
import { WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG } from "./wow335ShamanTidalWavesRuntime";
|
|
|
|
function stableUnit(seed: string): number {
|
|
let hash = 2_166_136_261;
|
|
for (const character of seed) {
|
|
hash ^= character.charCodeAt(0);
|
|
hash = Math.imul(hash, 16_777_619);
|
|
}
|
|
return (hash >>> 0) / 0x1_0000_0000;
|
|
}
|
|
|
|
function shamanAbility(name: string) {
|
|
const source = abilitiesForClass("shaman").find((candidate) => candidate.name === name);
|
|
expect(source, `generated Shaman ${name}`).toBeDefined();
|
|
return abilityAtLevel(source!, 80);
|
|
}
|
|
|
|
function onlyReady(member: PartyMember, abilityName: string): PartyMember {
|
|
const readyAbility = shamanAbility(abilityName);
|
|
return {
|
|
...member,
|
|
cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((candidate) => [
|
|
candidate.id,
|
|
candidate.id === readyAbility.id ? 0 : 1_000_000,
|
|
])),
|
|
activeCast: null,
|
|
activeCastTargetId: null,
|
|
nextActionAt: 0,
|
|
};
|
|
}
|
|
|
|
function noAbilitiesReady(member: PartyMember): PartyMember {
|
|
return {
|
|
...member,
|
|
cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((candidate) => [candidate.id, 1_000_000])),
|
|
activeCast: null,
|
|
activeCastTargetId: null,
|
|
nextActionAt: 0,
|
|
};
|
|
}
|
|
|
|
async function setupShamanParty(options: {
|
|
readonly tidalRank?: number;
|
|
readonly riptideRank?: number;
|
|
readonly waterShieldRank?: number;
|
|
readonly improvedShieldsRank?: number;
|
|
readonly elementalFocusRank?: number;
|
|
readonly secondShaman?: boolean;
|
|
readonly secondShamanTidalRank?: number;
|
|
readonly spellPower?: number;
|
|
readonly criticalStrikeRating?: number;
|
|
} = {}) {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
await preloadAbilityCatalogsForClasses(["shaman", "warrior"]);
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
|
useCombatStore.setState({ auras: [], effects: [], combatEvents: [] });
|
|
usePartyStore.getState().initializeParty("shaman-party-runtime", 80, useCombatStore.getState().health, "damage", 3);
|
|
|
|
const talentRanks = {
|
|
"talent-2063": options.tidalRank ?? 0,
|
|
"talent-2064": options.riptideRank ?? 0,
|
|
"talent-583": options.waterShieldRank ?? 0,
|
|
"talent-607": options.improvedShieldsRank ?? 0,
|
|
"talent-574": options.elementalFocusRank ?? 0,
|
|
};
|
|
const firstShamanBase = createConfiguredPartyMember({
|
|
name: "Party Shaman",
|
|
classId: "shaman",
|
|
specialization: "Restoration",
|
|
specializationId: "shaman:restoration",
|
|
talentTreeId: "shaman-restoration",
|
|
talentRanks,
|
|
role: "healer",
|
|
}, 80, 0, "shaman-party-runtime:first");
|
|
const spellPower = options.spellPower ?? 0;
|
|
const criticalStrikeRating = options.criticalStrikeRating ?? 0;
|
|
const firstShaman: PartyMember = {
|
|
...onlyReady(firstShamanBase, "Healing Wave"),
|
|
gearStats: { ...EMPTY_ITEM_STATS, spellPower, criticalStrikeRating },
|
|
resource: 10_000,
|
|
maxResource: 20_000,
|
|
resourcePools: { ...firstShamanBase.resourcePools, mana: 10_000 },
|
|
health: firstShamanBase.maxHealth,
|
|
};
|
|
const tankBase = createConfiguredPartyMember({
|
|
name: "Party Tank",
|
|
classId: "warrior",
|
|
specialization: "Protection",
|
|
specializationId: "warrior:protection",
|
|
talentTreeId: "warrior-protection",
|
|
role: "tank",
|
|
}, 80, 1, "shaman-party-runtime:tank");
|
|
const tank: PartyMember = {
|
|
...tankBase,
|
|
health: Math.round(tankBase.maxHealth * 0.25),
|
|
activeCast: null,
|
|
activeCastTargetId: null,
|
|
nextActionAt: 0,
|
|
};
|
|
const members: PartyMember[] = [firstShaman, tank];
|
|
if (options.secondShaman) {
|
|
const secondBase = createConfiguredPartyMember({
|
|
name: "Second Party Shaman",
|
|
classId: "shaman",
|
|
specialization: "Restoration",
|
|
specializationId: "shaman:restoration",
|
|
talentTreeId: "shaman-restoration",
|
|
talentRanks: {
|
|
"talent-2063": options.secondShamanTidalRank ?? 0,
|
|
"talent-2064": 1,
|
|
"talent-583": 0,
|
|
},
|
|
role: "healer",
|
|
}, 80, 2, "shaman-party-runtime:second");
|
|
members.push({
|
|
...onlyReady(secondBase, "Healing Wave"),
|
|
gearStats: { ...EMPTY_ITEM_STATS },
|
|
resource: 10_000,
|
|
maxResource: 20_000,
|
|
resourcePools: { ...secondBase.resourcePools, mana: 10_000 },
|
|
health: secondBase.maxHealth,
|
|
});
|
|
}
|
|
usePartyStore.setState({ members });
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
for (const member of usePartyStore.getState().members) registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
|
return usePartyStore.getState().members;
|
|
}
|
|
|
|
function findPartyHealTime(
|
|
memberId: string,
|
|
abilityId: string,
|
|
targetId: string,
|
|
predicate: (roll: number, now: number) => boolean,
|
|
from = 20_000,
|
|
): number {
|
|
for (let now = from; now < from + 100_000; now += 1) {
|
|
if (predicate(stableUnit(`${memberId}:${abilityId}:${targetId}:${now}:wow335-shaman-improved-water-shield`), now)) {
|
|
return now;
|
|
}
|
|
}
|
|
throw new Error("Could not find a seeded Shaman party healing proc timestamp.");
|
|
}
|
|
|
|
describe("party WoW Shaman healing talents", () => {
|
|
it("grants and consumes party Elemental Focus Clearcasting after a critical elemental spell", async () => {
|
|
const members = await setupShamanParty({ elementalFocusRank: 2, criticalStrikeRating: 1_000_000 });
|
|
const shaman = members[0];
|
|
const bolt = shamanAbility("Lightning Bolt");
|
|
const targetId = "shaman-elemental-focus-party-target";
|
|
useCombatStore.getState().registerMob(targetId, {
|
|
name: "Elemental Focus target",
|
|
level: 80,
|
|
maxHealth: 100_000,
|
|
xpReward: 0,
|
|
});
|
|
setMobPosition(targetId, [2, 0, 0]);
|
|
useCombatStore.getState().damageMob(targetId, 1, 500);
|
|
updatePartyRuntimeTargetVisibility(shaman.id, targetId, true);
|
|
const damageShaman: PartyMember = {
|
|
...onlyReady(shaman, "Lightning Bolt"),
|
|
role: "damage",
|
|
health: shaman.maxHealth,
|
|
globalCooldownEndsAt: 0,
|
|
nextActionAt: 0,
|
|
};
|
|
const fullManaCost = partyAbilityCosts(damageShaman, bolt, 1_000)
|
|
.find((cost) => cost.resource === "mana")?.amount;
|
|
usePartyStore.setState((state) => ({
|
|
command: "attack",
|
|
activeMobId: targetId,
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? damageShaman
|
|
: { ...member, cooldowns: Object.fromEntries(abilitiesForClass("warrior").map((ability) => [ability.id, 1_000_000])) }),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
|
|
advanceDungeonPartyCombat(1_000);
|
|
const firstCast = usePartyStore.getState().members.find((member) => member.id === shaman.id)?.activeCast;
|
|
expect(firstCast?.abilityId).toBe(bolt.id);
|
|
advanceDungeonPartyCombat(firstCast!.completesAt);
|
|
|
|
const hit = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "damage" && event.sourceActorId === shaman.id && event.abilityId === bolt.id
|
|
));
|
|
expect(hit).toMatchObject({ critical: true });
|
|
const clearcasting = () => useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === shaman.id
|
|
&& aura.targetId === shaman.id
|
|
&& aura.definition.utilityTags?.includes("wow335-shaman-elemental-focus-clearcasting")
|
|
));
|
|
expect(clearcasting()).toMatchObject({ stacks: 2, expiresAt: hit!.occurredAt + 15_000 });
|
|
|
|
expect(fullManaCost).toBeGreaterThan(0);
|
|
expect(partyAbilityCosts(shaman, bolt, hit!.occurredAt)
|
|
.find((cost) => cost.resource === "mana")?.amount).toBe(Math.round(fullManaCost! * 0.7));
|
|
|
|
const currentShaman = usePartyStore.getState().members.find((member) => member.id === shaman.id)!;
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? { ...onlyReady(currentShaman, "Lightning Bolt"), globalCooldownEndsAt: 0, nextActionAt: 0 }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(hit!.occurredAt + 1_000);
|
|
expect(usePartyStore.getState().members.find((member) => member.id === shaman.id)?.activeCast?.abilityId)
|
|
.toBe(bolt.id);
|
|
expect(clearcasting()?.stacks).toBe(1);
|
|
});
|
|
|
|
it("procs rank-owned Improved Water Shield from its own critical Healing Wave and restores only its mana", async () => {
|
|
const members = await setupShamanParty({
|
|
tidalRank: 3,
|
|
riptideRank: 1,
|
|
waterShieldRank: 3,
|
|
improvedShieldsRank: 3,
|
|
spellPower: 1_000,
|
|
criticalStrikeRating: 100_000,
|
|
secondShaman: true,
|
|
});
|
|
const shaman = members[0];
|
|
const tank = members[1];
|
|
const secondShaman = members[2];
|
|
const playerManaBefore = useCombatStore.getState().resourcePools.mana;
|
|
const riptide = shamanAbility("Riptide");
|
|
const healingWave = shamanAbility("Healing Wave");
|
|
const riptideAt = findPartyHealTime(
|
|
shaman.id,
|
|
riptide.id,
|
|
tank.id,
|
|
(roll, now) => roll >= 0.3 && stableUnit(
|
|
`${shaman.id}:${healingWave.id}:${tank.id}:${now + 3_500}:wow335-shaman-improved-water-shield`,
|
|
) < 0.6,
|
|
);
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Riptide")
|
|
: member.id === secondShaman.id ? noAbilitiesReady(member) : member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(riptideAt);
|
|
const tidalAura = useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === shaman.id
|
|
&& aura.targetId === shaman.id
|
|
&& aura.definition.utilityTags?.includes(WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG)
|
|
));
|
|
expect(tidalAura).toMatchObject({ stacks: 2, expiresAt: riptideAt + 15_000 });
|
|
expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({
|
|
kind: "hot",
|
|
sourceActorId: shaman.id,
|
|
sourceAbilityId: riptide.id,
|
|
targetId: tank.id,
|
|
}));
|
|
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Healing Wave")
|
|
: member.id === secondShaman.id ? noAbilitiesReady(member) : member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(riptideAt + 1_500);
|
|
const activeCast = usePartyStore.getState().members.find((member) => member.id === shaman.id)?.activeCast;
|
|
expect(activeCast).toMatchObject({
|
|
abilityId: healingWave.id,
|
|
originalDurationMs: Math.round(healingWave.castTimeMs * 0.8),
|
|
tidalWavesChargesConsumed: 2,
|
|
});
|
|
expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === tidalAura?.instanceId)?.stacks).toBe(1);
|
|
|
|
advanceDungeonPartyCombat(activeCast!.completesAt);
|
|
|
|
const combat = useCombatStore.getState();
|
|
const healingEvent = [...combat.combatEvents].reverse().find((event) => (
|
|
event.kind === "healing" && event.sourceActorId === shaman.id && event.abilityId === healingWave.id
|
|
));
|
|
const waterShieldEvent = [...combat.combatEvents].reverse().find((event) => (
|
|
event.kind === "resource" && event.sourceActorId === shaman.id
|
|
&& event.abilityId === "wow335-shaman-improved-water-shield"
|
|
));
|
|
const effect = healingWave.effects.find((candidate) => candidate.kind === "heal");
|
|
expect(effect).toBeDefined();
|
|
const ordinaryHeal = partyAbilityAmount(shaman, healingWave, effect!);
|
|
expect(healingEvent).toMatchObject({ targetActorId: tank.id, school: "nature", critical: true });
|
|
expect(healingEvent?.rawAmount).toBe(Math.round((ordinaryHeal + 120) * 1.5));
|
|
expect(waterShieldEvent).toMatchObject({
|
|
targetActorId: shaman.id,
|
|
rawAmount: 460,
|
|
effectiveAmount: 460,
|
|
overheal: 0,
|
|
});
|
|
expect(combat.combatEvents).not.toContainEqual(expect.objectContaining({
|
|
kind: "resource",
|
|
sourceActorId: secondShaman.id,
|
|
abilityId: "wow335-shaman-improved-water-shield",
|
|
}));
|
|
expect(combat.actors[shaman.id]?.resources.mana.current).toBe(
|
|
usePartyStore.getState().members.find((member) => member.id === shaman.id)?.resourcePools.mana,
|
|
);
|
|
expect(combat.resourcePools.mana).toBe(playerManaBefore);
|
|
expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({
|
|
kind: "hot",
|
|
sourceActorId: shaman.id,
|
|
sourceAbilityId: riptide.id,
|
|
targetId: tank.id,
|
|
}));
|
|
});
|
|
|
|
it("adds party Tidal Waves charges to Lesser Healing Wave crit chance and consumes one charge", async () => {
|
|
const members = await setupShamanParty({ tidalRank: 3, riptideRank: 1 });
|
|
const shaman = members[0];
|
|
const tank = members[1];
|
|
const riptide = shamanAbility("Riptide");
|
|
const lesserHealingWave = shamanAbility("Lesser Healing Wave");
|
|
let riptideAt = 1_000;
|
|
for (; riptideAt < 100_000; riptideAt += 1) {
|
|
if (stableUnit(`${shaman.id}:${lesserHealingWave.id}:${tank.id}:${riptideAt + 3_000}:heal`) < 0.2) break;
|
|
}
|
|
expect(riptideAt).toBeLessThan(100_000);
|
|
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id ? onlyReady(member, "Riptide") : member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(riptideAt);
|
|
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
|
|
sourceId: shaman.id,
|
|
targetId: shaman.id,
|
|
stacks: 2,
|
|
definition: expect.objectContaining({ utilityTags: expect.arrayContaining([WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG]) }),
|
|
}));
|
|
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Lesser Healing Wave")
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(riptideAt + 1_500);
|
|
const activeCast = usePartyStore.getState().members.find((member) => member.id === shaman.id)?.activeCast;
|
|
expect(activeCast).toMatchObject({
|
|
abilityId: lesserHealingWave.id,
|
|
originalDurationMs: lesserHealingWave.castTimeMs,
|
|
tidalWavesChargesConsumed: 2,
|
|
});
|
|
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
|
|
sourceId: shaman.id,
|
|
targetId: shaman.id,
|
|
stacks: 1,
|
|
definition: expect.objectContaining({ utilityTags: expect.arrayContaining([WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG]) }),
|
|
}));
|
|
advanceDungeonPartyCombat(activeCast!.completesAt);
|
|
|
|
const healingEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "healing" && event.sourceActorId === shaman.id && event.abilityId === lesserHealingWave.id
|
|
));
|
|
expect(healingEvent).toMatchObject({ targetActorId: tank.id, critical: true });
|
|
});
|
|
|
|
it("expires party Tidal Waves after 15 seconds and ignores another Shaman's Riptide HoT", async () => {
|
|
const members = await setupShamanParty({ tidalRank: 3, riptideRank: 1, secondShaman: true });
|
|
const shaman = members[0];
|
|
const tank = members[1];
|
|
const secondShaman = members[2];
|
|
const riptide = shamanAbility("Riptide");
|
|
const chainHeal = shamanAbility("Chain Heal");
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Riptide")
|
|
: member.id === secondShaman.id
|
|
? { ...onlyReady(member, "Healing Wave"), cooldowns: Object.fromEntries(
|
|
abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000]),
|
|
) }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
const riptideAt = 1_000;
|
|
advanceDungeonPartyCombat(riptideAt);
|
|
const aura = useCombatStore.getState().auras.find((candidate) => (
|
|
candidate.sourceId === shaman.id
|
|
&& candidate.targetId === shaman.id
|
|
&& candidate.definition.utilityTags?.includes(WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG)
|
|
));
|
|
expect(aura).toMatchObject({ stacks: 2, expiresAt: 16_000 });
|
|
|
|
const initialHot = useCombatStore.getState().effects.find((effect) => (
|
|
effect.kind === "hot" && effect.sourceActorId === shaman.id && effect.targetId === tank.id
|
|
));
|
|
expect(initialHot?.sourceAbilityId).toBe(riptide.id);
|
|
const chainEffect = chainHeal.effects.find((effect) => effect.kind === "heal");
|
|
expect(chainEffect).toBeDefined();
|
|
const baseAmount = partyAbilityAmount(secondShaman, chainHeal, chainEffect!);
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === secondShaman.id
|
|
? onlyReady(member, "Chain Heal")
|
|
: member.id === shaman.id
|
|
? { ...member, cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000])) }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(riptideAt + 1_000);
|
|
const secondCast = usePartyStore.getState().members.find((member) => member.id === secondShaman.id)?.activeCast;
|
|
expect(secondCast?.abilityId).toBe(chainHeal.id);
|
|
advanceDungeonPartyCombat(secondCast!.completesAt);
|
|
const secondHeal = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "healing" && event.sourceActorId === secondShaman.id
|
|
&& event.abilityId === chainHeal.id && event.targetActorId === tank.id
|
|
));
|
|
expect(secondHeal?.rawAmount).toBe(baseAmount);
|
|
expect(useCombatStore.getState().auras).not.toContainEqual(expect.objectContaining({
|
|
sourceId: secondShaman.id,
|
|
targetId: secondShaman.id,
|
|
definition: expect.objectContaining({ utilityTags: expect.arrayContaining([WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG]) }),
|
|
}));
|
|
expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({
|
|
id: initialHot?.id,
|
|
sourceActorId: shaman.id,
|
|
targetId: tank.id,
|
|
}));
|
|
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Healing Wave")
|
|
: member.id === secondShaman.id
|
|
? { ...member, cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000])) }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(16_000);
|
|
const expiredCast = usePartyStore.getState().members.find((member) => member.id === shaman.id)?.activeCast;
|
|
expect(expiredCast?.abilityId).toBe(shamanAbility("Healing Wave").id);
|
|
expect(expiredCast?.originalDurationMs).toBe(shamanAbility("Healing Wave").castTimeMs);
|
|
expect(useCombatStore.getState().auras.find((candidate) => candidate.instanceId === aura?.instanceId)?.stacks).toBe(2);
|
|
});
|
|
|
|
it("consumes only the Riptide caster's matching HoT for the Chain Heal primary target", async () => {
|
|
const members = await setupShamanParty({
|
|
riptideRank: 1,
|
|
secondShaman: true,
|
|
secondShamanTidalRank: 3,
|
|
});
|
|
const shaman = members[0];
|
|
const tank = members[1];
|
|
const secondShaman = members[2];
|
|
const riptide = shamanAbility("Riptide");
|
|
const chainHeal = shamanAbility("Chain Heal");
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Riptide")
|
|
: member.id === secondShaman.id
|
|
? { ...onlyReady(member, "Healing Wave"), cooldowns: Object.fromEntries(
|
|
abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000]),
|
|
) }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(1_000);
|
|
const riptideHot = useCombatStore.getState().effects.find((effect) => (
|
|
effect.kind === "hot" && effect.sourceActorId === shaman.id
|
|
&& effect.sourceAbilityId === riptide.id && effect.targetId === tank.id
|
|
));
|
|
expect(riptideHot).toBeDefined();
|
|
|
|
const baseAmount = partyAbilityAmount(secondShaman, chainHeal, chainHeal.effects.find((effect) => effect.kind === "heal")!);
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === secondShaman.id
|
|
? onlyReady(member, "Chain Heal")
|
|
: member.id === shaman.id
|
|
? { ...member, cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000])) }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(2_500);
|
|
const otherCasterCast = usePartyStore.getState().members.find((member) => member.id === secondShaman.id)?.activeCast;
|
|
advanceDungeonPartyCombat(otherCasterCast!.completesAt);
|
|
const otherCasterHeal = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "healing" && event.sourceActorId === secondShaman.id
|
|
&& event.abilityId === chainHeal.id && event.targetActorId === tank.id
|
|
));
|
|
expect(otherCasterHeal?.rawAmount).toBe(baseAmount);
|
|
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
|
|
sourceId: secondShaman.id,
|
|
targetId: secondShaman.id,
|
|
stacks: 3,
|
|
definition: expect.objectContaining({
|
|
utilityTags: expect.arrayContaining([WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG]),
|
|
}),
|
|
}));
|
|
expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({ id: riptideHot?.id }));
|
|
|
|
usePartyStore.setState((state) => ({
|
|
members: state.members.map((member) => member.id === shaman.id
|
|
? onlyReady(member, "Chain Heal")
|
|
: member.id === secondShaman.id
|
|
? { ...member, cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000])) }
|
|
: member),
|
|
}));
|
|
useCombatStore.getState().commitCompanionActors(usePartyStore.getState().members);
|
|
advanceDungeonPartyCombat(otherCasterCast!.completesAt + 1_000);
|
|
const ownCasterCast = usePartyStore.getState().members.find((member) => member.id === shaman.id)?.activeCast;
|
|
advanceDungeonPartyCombat(ownCasterCast!.completesAt);
|
|
const ownCasterHeal = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "healing" && event.sourceActorId === shaman.id
|
|
&& event.abilityId === chainHeal.id && event.targetActorId === tank.id
|
|
));
|
|
expect(ownCasterHeal?.rawAmount).toBe(Math.round(baseAmount * 1.25));
|
|
expect(useCombatStore.getState().effects).not.toContainEqual(expect.objectContaining({ id: riptideHot?.id }));
|
|
});
|
|
});
|