Files
healer-man/src/game/wow335ShamanNaturesGuardian.integration.test.ts
T

329 lines
15 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { abilitiesForClass, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
clearPartyRuntimeRegistry,
registerPartyRuntimePosition,
updatePartyRuntimeTargetVisibility,
} from "./partyRuntimeRegistry";
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
import { EMPTY_ITEM_STATS } from "./itemStats";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import {
WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG,
WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG,
} from "./wow335ShamanNaturesGuardianRuntime";
const TARGET_ID = "wow335-shaman-natures-guardian-target";
const ENEMY_HIT_AT = 1_550;
const PLAYER_NEXT_ACTION_AT = 2_000;
function natureGuardianAuras(actorId: string) {
return useCombatStore.getState().auras.filter((aura) => aura.sourceId === actorId && aura.targetId === actorId);
}
function armShamanForHit(targetActorId: string): void {
const actorId = targetActorId === PLAYER_AURA_ENTITY_ID ? PLAYER_AGGRO_ID : targetActorId;
useCombatStore.getState().registerMob(TARGET_ID, {
name: "Nature's Guardian target",
level: 80,
maxHealth: 1_000_000,
xpReward: 0,
attacks: [{
...DEFAULT_ENEMY_ATTACK,
id: "nature-guardian-incoming-hit",
name: "Nature Guardian hit",
delivery: "projectile",
school: "physical",
range: 30,
damageMultiplier: 6,
}],
});
setMobPosition(TARGET_ID, [2, 0, 0]);
useCombatStore.getState().damageMob(
TARGET_ID,
1,
100,
{ actorId, role: "damage", abilityId: "nature-guardian-test-engage" },
"physical",
);
}
describe("WoW Shaman Nature's Guardian player and companion integration", () => {
beforeEach(async () => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetPartyRuntimeClock();
await preloadAbilityCatalogsForClasses(["shaman", "mage"]);
useCombatStore.getState().initializeCharacter({ classId: "shaman", role: "damage", level: 80, xp: 0 });
useCombatStore.setState({ auras: [], effects: [], combatEvents: [] });
usePartyStore.getState().initializeParty(
"wow-shaman-natures-guardian-player",
80,
useCombatStore.getState().health,
"damage",
1,
);
});
it("arms on the player's direct hit below 30%, then spends one charge on a Nature damage spell", () => {
useCombatStore.setState((state) => ({
health: Math.round(state.maxHealth * 0.4),
talentRanks: { ...state.talentRanks, "talent-1699": 3 },
}));
armShamanForHit(PLAYER_AURA_ENTITY_ID);
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
expect(useCombatStore.getState().health).toBeGreaterThan(0);
expect(useCombatStore.getState().health).toBeLessThan(useCombatStore.getState().maxHealth * 0.3);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID)).toEqual(expect.arrayContaining([
expect.objectContaining({ definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG]),
}) }),
expect.objectContaining({ definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG]),
}) }),
]));
const lightningBolt = abilitiesForClass("shaman").find((ability) => ability.name === "Lightning Bolt");
expect(lightningBolt).toBeDefined();
const cast = useCombatStore.getState().castAbility(lightningBolt!.id, { targetActorId: TARGET_ID, now: PLAYER_NEXT_ACTION_AT });
expect(cast.ok, cast.reason).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt);
const damageEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID && event.abilityId === lightningBolt!.id
));
expect(damageEvent?.critical).toBe(true);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(false);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG)
))).toBe(true);
});
it("spends the player's charge on an accepted Nature healing spell and guarantees its critical heal", () => {
useCombatStore.setState((state) => ({
health: Math.round(state.maxHealth * 0.4),
talentRanks: { ...state.talentRanks, "talent-1699": 3 },
}));
armShamanForHit(PLAYER_AURA_ENTITY_ID);
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID)).toEqual(expect.arrayContaining([
expect.objectContaining({ definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG]),
}) }),
]));
const healingWave = abilitiesForClass("shaman").find((ability) => ability.name === "Healing Wave");
expect(healingWave).toBeDefined();
const rejected = useCombatStore.getState().castAbility(healingWave!.id, {
targetActorId: "missing-friendly-target",
now: PLAYER_NEXT_ACTION_AT,
});
expect(rejected.ok).toBe(false);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(true);
const cast = useCombatStore.getState().castAbility(healingWave!.id, {
now: PLAYER_NEXT_ACTION_AT,
});
expect(cast.ok, cast.reason).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
expect(activeCast, "Healing Wave remains charged while casting").toBeDefined();
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(true);
expect(useCombatStore.getState().cancelCast("test interruption", PLAYER_NEXT_ACTION_AT + 500)).toBe(true);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(true);
const resolvedCast = useCombatStore.getState().castAbility(healingWave!.id, {
now: PLAYER_NEXT_ACTION_AT + 2_000,
});
expect(resolvedCast.ok, resolvedCast.reason).toBe(true);
const resolvedActiveCast = useCombatStore.getState().activeCast;
expect(resolvedActiveCast, "the retry begins a new Healing Wave cast").toBeDefined();
useCombatStore.getState().tick(0.1, resolvedActiveCast!.completesAt);
const healingEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
event.kind === "healing" && event.sourceActorId === PLAYER_AGGRO_ID
&& event.targetActorId === PLAYER_AURA_ENTITY_ID && event.abilityId === healingWave!.id
));
expect(healingEvent?.critical).toBe(true);
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(false);
});
it("uses the companion's own rank for the incoming proc and consumes one charge after its accepted Nature spell", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80, xp: 0 });
usePartyStore.getState().initializeParty(
"wow-shaman-natures-guardian-companion",
80,
useCombatStore.getState().health,
"damage",
1,
);
const lightningBolt = abilitiesForClass("shaman").find((ability) => ability.name === "Lightning Bolt");
expect(lightningBolt).toBeDefined();
const shamanBase = createConfiguredPartyMember({
name: "Nature's Guardian Shaman",
classId: "shaman",
specialization: "Elemental",
specializationId: "shaman:elemental",
talentTreeId: "shaman-elemental",
talentRanks: { "talent-1699": 3 },
role: "damage",
}, 80, 0, "wow-shaman-natures-guardian-companion");
const disabledCooldowns = Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000]));
const member = {
...shamanBase,
gearStats: EMPTY_ITEM_STATS,
health: Math.round(shamanBase.maxHealth * 0.4),
cooldowns: disabledCooldowns,
nextActionAt: 1_000_000,
activeCast: null,
activeCastTargetId: null,
status: "ready" as const,
};
usePartyStore.setState({ members: [member], command: "attack", activeMobId: TARGET_ID });
useCombatStore.getState().commitCompanionActors([member]);
registerPartyRuntimePosition(member.id, [0, 0, 0]);
useCombatStore.setState((state) => ({
actors: {
...state.actors,
[member.id]: { ...state.actors[member.id]!, health: member.health, alive: true },
},
}));
armShamanForHit(member.id);
updatePartyRuntimeTargetVisibility(member.id, TARGET_ID, true);
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
const actorAfterHit = useCombatStore.getState().actors[member.id];
expect(actorAfterHit?.health).toBeGreaterThan(0);
expect(actorAfterHit?.health).toBeLessThan(actorAfterHit!.maxHealth * 0.3);
expect(natureGuardianAuras(member.id)).toEqual(expect.arrayContaining([
expect.objectContaining({ definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG]),
}) }),
expect.objectContaining({ definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG]),
}) }),
]));
const readyForBolt = {
...usePartyStore.getState().members[0]!,
cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [
ability.id,
ability.id === lightningBolt!.id ? 0 : 1_000_000,
])),
nextActionAt: PLAYER_NEXT_ACTION_AT,
activeCast: null,
activeCastTargetId: null,
};
usePartyStore.setState({ members: [readyForBolt] });
useCombatStore.getState().commitCompanionActors([readyForBolt]);
advanceDungeonPartyCombat(PLAYER_NEXT_ACTION_AT);
const started = usePartyStore.getState().members[0]?.activeCast;
expect(started, "the companion begins Lightning Bolt").toBeDefined();
advanceDungeonPartyCombat(started!.completesAt);
const damageEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
event.kind === "damage" && event.sourceActorId === member.id && event.abilityId === lightningBolt!.id
));
expect(damageEvent?.critical).toBe(true);
expect(natureGuardianAuras(member.id).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(false);
expect(natureGuardianAuras(member.id).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG)
))).toBe(true);
});
it("spends the companion's charge on its accepted Nature healing spell", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80, xp: 0 });
usePartyStore.getState().initializeParty(
"wow-shaman-natures-guardian-companion-heal",
80,
useCombatStore.getState().health,
"damage",
1,
);
const healingWave = abilitiesForClass("shaman").find((ability) => ability.name === "Healing Wave");
expect(healingWave).toBeDefined();
const shamanBase = createConfiguredPartyMember({
name: "Nature's Guardian Healing Shaman",
classId: "shaman",
specialization: "Restoration",
specializationId: "shaman:restoration",
talentTreeId: "shaman-restoration",
talentRanks: { "talent-1699": 3 },
role: "healer",
}, 80, 0, "wow-shaman-natures-guardian-companion-heal");
const disabledCooldowns = Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000]));
const member = {
...shamanBase,
gearStats: EMPTY_ITEM_STATS,
health: Math.round(shamanBase.maxHealth * 0.4),
cooldowns: disabledCooldowns,
nextActionAt: 1_000_000,
activeCast: null,
activeCastTargetId: null,
status: "ready" as const,
};
usePartyStore.setState({ members: [member], command: "attack", activeMobId: TARGET_ID });
useCombatStore.getState().commitCompanionActors([member]);
registerPartyRuntimePosition(member.id, [0, 0, 0]);
useCombatStore.setState((state) => ({
actors: {
...state.actors,
[member.id]: { ...state.actors[member.id]!, health: member.health, alive: true },
},
}));
armShamanForHit(member.id);
updatePartyRuntimeTargetVisibility(member.id, TARGET_ID, true);
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
const actorAfterHit = useCombatStore.getState().actors[member.id];
expect(actorAfterHit?.health).toBeLessThan(actorAfterHit!.maxHealth * 0.3);
expect(natureGuardianAuras(member.id).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(true);
const readyForHeal = {
...usePartyStore.getState().members[0]!,
health: actorAfterHit!.health,
cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [
ability.id,
ability.id === healingWave!.id ? 0 : 1_000_000,
])),
nextActionAt: PLAYER_NEXT_ACTION_AT,
activeCast: null,
activeCastTargetId: null,
};
usePartyStore.setState({ members: [readyForHeal] });
useCombatStore.getState().commitCompanionActors([readyForHeal]);
advanceDungeonPartyCombat(PLAYER_NEXT_ACTION_AT);
const activeCast = usePartyStore.getState().members[0]?.activeCast;
expect(activeCast?.abilityId).toBe(healingWave!.id);
expect(natureGuardianAuras(member.id).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(true);
advanceDungeonPartyCombat(activeCast!.completesAt);
const healingEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
event.kind === "healing" && event.sourceActorId === member.id
&& event.targetActorId === member.id && event.abilityId === healingWave!.id
));
expect(healingEvent?.critical).toBe(true);
expect(natureGuardianAuras(member.id).some((aura) => (
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
))).toBe(false);
});
});