198 lines
10 KiB
TypeScript
198 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { abilitiesForClass, abilityAtLevel, abilityTalentNodeId, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
|
import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { resetPartyRuntimeClock } from "./partyRuntime";
|
|
import { WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG } from "./wow335ShamanTidalWavesRuntime";
|
|
|
|
function shamanAbility(name: string) {
|
|
const source = abilitiesForClass("shaman").find((candidate) => candidate.name === name);
|
|
expect(source, `generated Shaman ${name}`).toBeDefined();
|
|
return abilityAtLevel(source!, 80);
|
|
}
|
|
|
|
function shieldAuraEffect(name: string) {
|
|
const ability = shamanAbility(name);
|
|
const effect = ability.effects.find((candidate) => candidate.kind === "apply-aura");
|
|
expect(effect, `${name} apply-aura`).toBeDefined();
|
|
if (!effect || effect.kind !== "apply-aura") throw new Error(`${name} aura is missing`);
|
|
return { ability, effect };
|
|
}
|
|
|
|
function registerAttacker(id: string, targetActorId: string, engagedAt = 100): void {
|
|
useCombatStore.getState().registerMob(id, {
|
|
name: id,
|
|
level: 80,
|
|
maxHealth: 100_000,
|
|
xpReward: 0,
|
|
attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: `${id}:swing`, range: 30 }],
|
|
});
|
|
setMobPosition(id, [2, 0, 0]);
|
|
useCombatStore.getState().damageMob(id, 1, engagedAt, { actorId: targetActorId, role: "tank" }, "physical");
|
|
}
|
|
|
|
describe("WoW Shaman shield proc integration", () => {
|
|
beforeEach(async () => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
await preloadAbilityCatalogsForClasses(["shaman", "warrior"]);
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
|
|
useCombatStore.setState({ auras: [], effects: [], combatEvents: [] });
|
|
usePartyStore.getState().initializeParty("shaman-shields-test", 80, useCombatStore.getState().health, "damage", 1);
|
|
});
|
|
|
|
it("scales player Lightning Shield damage and Earth Shield charges/healing, then gates Tidal Waves to its 5s ICD", () => {
|
|
useCombatStore.getState().initializeCharacter({ classId: "shaman", role: "healer", level: 80 });
|
|
useCombatStore.setState((state) => ({
|
|
health: Math.round(state.maxHealth * 0.5),
|
|
talentRanks: {
|
|
...state.talentRanks,
|
|
"talent-607": 3,
|
|
"talent-2059": 3,
|
|
"talent-2063": 3,
|
|
},
|
|
}));
|
|
|
|
const lightning = shieldAuraEffect("Lightning Shield");
|
|
const earth = shieldAuraEffect("Earth Shield");
|
|
const earthTalentNodeId = abilityTalentNodeId(earth.ability);
|
|
useCombatStore.setState((state) => ({
|
|
talentRanks: { ...state.talentRanks, ...(earthTalentNodeId ? { [earthTalentNodeId]: 1 } : {}) },
|
|
}));
|
|
const lightningCast = useCombatStore.getState().castAbility(lightning.ability.id, {
|
|
targetActorId: PLAYER_AURA_ENTITY_ID, now: 1_000,
|
|
});
|
|
expect(lightningCast.ok, lightningCast.reason).toBe(true);
|
|
const earthCast = useCombatStore.getState().castAbility(earth.ability.id, {
|
|
targetActorId: null, now: 4_000,
|
|
});
|
|
expect(earthCast.ok, earthCast.reason).toBe(true);
|
|
|
|
const earthAura = useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === PLAYER_AGGRO_ID && aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.procs?.some((proc) => proc.id.endsWith(":earth-shield"))
|
|
));
|
|
expect(earthAura, useCombatStore.getState().auras.map((aura) => `${aura.sourceId}:${aura.targetId}:${aura.definition.name}:${aura.definition.procs?.map((proc) => proc.id).join(",")}`).join(" | "))
|
|
.toBeDefined();
|
|
expect(earthAura?.definition.procs?.find((proc) => proc.id.endsWith(":earth-shield"))).toMatchObject({
|
|
charges: 12,
|
|
internalCooldownMs: 3_000,
|
|
});
|
|
|
|
registerAttacker("shaman-player-shield-attacker", PLAYER_AGGRO_ID, 4_000);
|
|
useCombatStore.getState().advanceMobCombat(5_450);
|
|
const lightningProc = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID
|
|
&& event.abilityId?.endsWith(":lightning-shield")
|
|
));
|
|
const lightningBase = lightning.effect.aura.procs?.find((proc) => proc.id.endsWith(":lightning-shield"))?.action;
|
|
expect(lightningBase?.kind).toBe("damage");
|
|
if (lightningBase?.kind !== "damage") throw new Error("Lightning Shield damage proc is missing");
|
|
expect(lightningProc?.rawAmount).toBe(Math.round(lightningBase.amount * 1.6));
|
|
expect(useCombatStore.getState().auras.find((aura) => aura.definition.exclusiveGroup === "shaman:elemental-shield")
|
|
?.procStates.find((proc) => proc.procId.endsWith(":lightning-shield"))?.remainingCharges).toBe(2);
|
|
|
|
useCombatStore.getState().advanceMobCombat(5_450);
|
|
const tidalAuraAtFirstHeal = useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === PLAYER_AGGRO_ID && aura.targetId === PLAYER_AGGRO_ID
|
|
&& aura.definition.utilityTags?.includes(WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG)
|
|
));
|
|
const earthProcEvents = () => useCombatStore.getState().combatEvents.filter((event) => (
|
|
event.kind === "healing" && event.sourceActorId === PLAYER_AGGRO_ID
|
|
&& event.abilityId?.endsWith(":earth-shield")
|
|
));
|
|
const earthBase = earthAura?.definition.procs?.find((proc) => proc.id.endsWith(":earth-shield"))?.action;
|
|
expect(earthBase?.kind).toBe("heal");
|
|
if (earthBase?.kind !== "heal") throw new Error("Earth Shield heal proc is missing");
|
|
expect(earthProcEvents()[0]?.rawAmount).toBe(Math.round(earthBase.amount * 1.3));
|
|
expect(tidalAuraAtFirstHeal?.stacks).toBe(1);
|
|
|
|
useCombatStore.getState().advanceMobCombat(7_350);
|
|
useCombatStore.getState().advanceMobCombat(9_250);
|
|
expect(useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === PLAYER_AGGRO_ID && aura.targetId === PLAYER_AGGRO_ID
|
|
&& aura.definition.utilityTags?.includes(WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG)
|
|
))?.stacks).toBe(1);
|
|
let now = 9_250;
|
|
while (earthProcEvents().length < 3 && now < 30_000) {
|
|
now += 1_900;
|
|
useCombatStore.getState().advanceMobCombat(now);
|
|
}
|
|
expect(earthProcEvents()).toHaveLength(3);
|
|
expect(earthProcEvents()[2].occurredAt - earthProcEvents()[0].occurredAt).toBeGreaterThanOrEqual(5_000);
|
|
expect(useCombatStore.getState().auras.find((aura) => (
|
|
aura.sourceId === PLAYER_AGGRO_ID && aura.targetId === PLAYER_AGGRO_ID
|
|
&& aura.definition.utilityTags?.includes(WOW335_SHAMAN_TIDAL_WAVES_AURA_TAG)
|
|
))?.stacks).toBe(2);
|
|
expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === earthAura?.instanceId)
|
|
?.procStates.find((proc) => proc.procId.endsWith(":earth-shield"))?.remainingCharges).toBe(9);
|
|
});
|
|
|
|
it("applies Improved Earth Shield charge and healing bonuses to a generated companion's shield proc", () => {
|
|
const shamanBase = createConfiguredPartyMember({
|
|
name: "Shield Shaman",
|
|
classId: "shaman",
|
|
specialization: "Restoration",
|
|
specializationId: "shaman:restoration",
|
|
talentTreeId: "shaman-restoration",
|
|
talentRanks: { "talent-607": 3, "talent-2059": 3, "talent-2063": 3 },
|
|
role: "healer",
|
|
}, 80, 0, "shaman-shields-test");
|
|
const tankBase = createConfiguredPartyMember({
|
|
name: "Shield Tank",
|
|
classId: "warrior",
|
|
specialization: "Protection",
|
|
specializationId: "warrior:protection",
|
|
talentTreeId: "warrior-protection",
|
|
role: "tank",
|
|
}, 80, 1, "shaman-shields-test");
|
|
const shaman = { ...shamanBase, gearStats: EMPTY_ITEM_STATS, health: shamanBase.maxHealth };
|
|
const tank = { ...tankBase, gearStats: EMPTY_ITEM_STATS, health: Math.round(tankBase.maxHealth * 0.35) };
|
|
usePartyStore.setState({ members: [shaman, tank] });
|
|
registerPartyRuntimePosition(shaman.id, [0, 0, 0]);
|
|
registerPartyRuntimePosition(tank.id, [0, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
|
|
const earth = shieldAuraEffect("Earth Shield");
|
|
for (const effect of earth.ability.effects.filter((candidate) => candidate.kind === "apply-aura")) {
|
|
if (effect.kind === "apply-aura") {
|
|
expect(useCombatStore.getState().applyActorAura(shaman.id, tank.id, earth.ability, effect, 500)).toBe(true);
|
|
}
|
|
}
|
|
const aura = useCombatStore.getState().auras.find((candidate) => (
|
|
candidate.sourceId === shaman.id && candidate.targetId === tank.id
|
|
&& candidate.definition.procs?.some((proc) => proc.id.endsWith(":earth-shield"))
|
|
));
|
|
expect(aura?.definition.procs?.find((proc) => proc.id.endsWith(":earth-shield"))).toMatchObject({
|
|
charges: 12,
|
|
internalCooldownMs: 3_000,
|
|
});
|
|
|
|
registerAttacker("shaman-party-shield-attacker", tank.id);
|
|
useCombatStore.getState().advanceMobCombat(1_450);
|
|
const procEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
|
event.kind === "healing" && event.sourceActorId === shaman.id
|
|
&& event.targetActorId === tank.id && event.abilityId?.endsWith(":earth-shield")
|
|
));
|
|
const baseAction = aura?.definition.procs?.find((proc) => proc.id.endsWith(":earth-shield"))?.action;
|
|
expect(baseAction?.kind).toBe("heal");
|
|
if (baseAction?.kind !== "heal") throw new Error("Earth Shield heal proc is missing");
|
|
expect(procEvent?.rawAmount).toBe(Math.round(baseAction.amount * 1.3));
|
|
expect(procEvent?.effectiveAmount).toBeGreaterThan(0);
|
|
expect(useCombatStore.getState().auras.find((candidate) => candidate.instanceId === aura?.instanceId)
|
|
?.procStates.find((proc) => proc.procId.endsWith(":earth-shield"))?.remainingCharges).toBe(11);
|
|
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]) }),
|
|
}));
|
|
});
|
|
});
|