Files
healer-man/src/game/wow335DruidProcRuntime.test.ts
2026-09-30 09:57:13 -04:00

602 lines
26 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { applyAura, resolveStatValue } from "./combatAuras";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import { preloadAbilityCatalogsForClasses } from "./abilityCatalog";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { usePartyStore } from "./partyStore";
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
const TARGET_ID = "druid-proc-target";
function unit(seed: string): number {
let hash = 2166136261;
for (let index = 0; index < seed.length; index += 1) {
hash ^= seed.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0) / 0x1_0000_0000;
}
function abilityId(name: string): string {
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
expect(ability, name).toBeDefined();
return ability!.id;
}
function findDruidProcStart(name: string, startAt: number, chance: number, procTag: string): number {
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
expect(ability, name).toBeDefined();
for (let start = startAt; start < startAt + 100_000; start += 100) {
const impactAt = start + ability!.castTimeMs;
const directSeed = `${PLAYER_AGGRO_ID}:${ability!.id}:${TARGET_ID}:${impactAt}:direct`;
if (unit(`${directSeed}:outcome`) > 0.1 && unit(`${directSeed}:${procTag}`) < chance) return start;
}
throw new Error(`No deterministic ${name} proc seed found.`);
}
function castAndFinish(name: string, startedAt: number) {
const id = abilityId(name);
const result = useCombatStore.getState().castAbility(id, { targetActorId: TARGET_ID, now: startedAt });
expect(result.ok, JSON.stringify(result)).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
if (activeCast?.abilityId === id) {
useCombatStore.getState().tick(Math.max(0.1, (activeCast.completesAt - startedAt) / 1_000), activeCast.completesAt);
}
return useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage" && event.abilityId === id
)).at(-1);
}
function guaranteeDruidMeleeCrits(): void {
useCombatStore.setState((state) => ({
auras: [...applyAura(state.auras, {
definition: {
id: "test-druid-guaranteed-melee-crit",
name: "Test Melee Critical Chance",
disposition: "buff",
durationMs: null,
modifiers: [
{ kind: "stat", stat: "hit", operation: "flat", value: 100 },
{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: 100 },
{ kind: "stat", stat: "spell-hit", operation: "flat", value: 100 },
{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 },
],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras],
mobs: {
...state.mobs,
[TARGET_ID]: {
...state.mobs[TARGET_ID]!,
characterDefender: {
level: 80,
armor: 0,
dodgePercent: 0,
parryPercent: 0,
blockPercent: 0,
blockValue: 0,
},
},
},
}));
}
function findDruidPrimalFurySeed(procTag: string, chance: number, shouldProc: boolean): string {
for (let index = 0; index < 100_000; index += 1) {
const seed = `druid-primal-fury:${procTag}:${index}`;
if ((unit(`${seed}:wow335-druid-primal-fury-${procTag}`) < chance) === shouldProc) return seed;
}
throw new Error(`No deterministic Primal Fury ${procTag} seed found.`);
}
describe("WoW Druid proc runtime", () => {
beforeEach(async () => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
await preloadAbilityCatalogsForClasses(["druid"]);
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
useCombatStore.getState().resetSettings();
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty("wow335-druid-proc-tests", 1, useCombatStore.getState().health);
useCombatStore.setState((state) => ({
resource: state.maxResource,
resourcePools: { ...state.resourcePools, mana: state.maxResourcePools.mana },
}));
useCombatStore.getState().registerMob(TARGET_ID, { name: "Target", maxHealth: 1_000_000 });
setMobPosition(TARGET_ID, [4, 0, 0]);
useCombatStore.getState().selectMob(TARGET_ID);
});
it("procs Solar Eclipse from Starfire and gives Wrath its source 30% damage bonus", () => {
const baseline = castAndFinish("Wrath", 1_000);
expect(baseline?.kind).toBe("damage");
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-1924": 3 } }));
castAndFinish("Starfire", 5_000);
const solar = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-eclipse-solar")
));
expect(solar?.expiresAt).toBe(23_500);
expect(useCombatStore.getState().cooldowns["wow335-druid-eclipse-solar"]).toBe(38_500);
const buffed = castAndFinish("Wrath", 10_000);
expect(buffed?.kind).toBe("damage");
expect(buffed?.rawAmount).toBe(Math.round(baseline!.rawAmount * 1.3));
});
it("procs Lunar Eclipse from Wrath and adds 30 percentage points to Starfire crit chance", () => {
const baselineEvent = castAndFinish("Starfire", 1_000);
expect(baselineEvent?.kind).toBe("damage");
const baselineCritChance = useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent;
expect(baselineCritChance).toBeDefined();
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-1924": 3 } }));
const wrathStart = findDruidProcStart("Wrath", 6_000, 0.6, "druid-eclipse");
castAndFinish("Wrath", wrathStart);
const lunar = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-eclipse-lunar")
));
expect(lunar).toBeDefined();
castAndFinish("Starfire", wrathStart + 2_100);
expect(useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent)
.toBe(baselineCritChance! + 30);
});
it("grants Nature's Grace after a direct healing critical and applies 20% cast haste for four seconds", () => {
const healingTouch = abilityId("Healing Touch");
let startedAt = 1_000;
while (unit(`${PLAYER_AGGRO_ID}:${healingTouch}:${PLAYER_AURA_ENTITY_ID}:${startedAt + 3_000}:heal:druid-omen-of-clarity`) >= 0.06) {
startedAt += 100;
}
useCombatStore.setState((state) => ({
health: Math.round(state.maxHealth / 2),
talentRanks: { ...state.talentRanks, "talent-789": 3, "talent-827": 3 },
auras: [...applyAura(state.auras, {
definition: {
id: "test-druid-guaranteed-spell-crit",
name: "Test Critical Chance",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: startedAt,
}).auras],
}));
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt }).ok).toBe(true);
const firstCast = useCombatStore.getState().activeCast;
expect(firstCast?.abilityId).toBe(healingTouch);
useCombatStore.getState().tick(0.1, firstCast!.completesAt);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "healing" && event.abilityId === healingTouch && event.critical
))).toBe(true);
const natureGrace = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-natures-grace")
));
expect(natureGrace?.expiresAt).toBe(firstCast!.completesAt + 4_000);
expect(resolveStatValue(0, [natureGrace!], "haste")).toBe(20);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
))).toBe(true);
const manaAfterFirstHeal = useCombatStore.getState().resource;
const nextStart = firstCast!.completesAt + 1_500;
useCombatStore.setState((state) => ({
lastResourceSpentAt: { ...state.lastResourceSpentAt, mana: nextStart },
}));
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: nextStart }).ok).toBe(true);
expect(useCombatStore.getState().resource).toBe(manaAfterFirstHeal);
const hastedCast = useCombatStore.getState().activeCast;
expect(hastedCast?.originalDurationMs).toBe(Math.round(3_000 / 1.2));
useCombatStore.getState().tick(0.1, hastedCast!.completesAt);
const overhealClearcasting = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
));
expect(overhealClearcasting?.expiresAt).toBe(hastedCast!.completesAt + 15_000);
expect(useCombatStore.getState().cooldowns["wow335-druid-omen-of-clarity"])
.toBe(hastedCast!.completesAt + 2_000);
});
it("procs Omen of Clarity from damage, makes the next eligible cast free, and respects its two-second ICD", () => {
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-827": 3 } }));
const moonfire = useCombatStore.getState().abilities.find((ability) => ability.name === "Moonfire");
expect(moonfire).toBeDefined();
const start = (() => {
for (let now = 1_000; now < 100_000; now += 1) {
const seed = `${PLAYER_AGGRO_ID}:${moonfire!.id}:${TARGET_ID}:${now}:direct`;
if (unit(`${seed}:outcome`) > 0.1 && unit(`${seed}:druid-omen-of-clarity`) < 0.06) return now;
}
throw new Error("No deterministic Omen of Clarity proc seed found.");
})();
const first = useCombatStore.getState().castAbility(moonfire!.id, { targetActorId: TARGET_ID, now: start });
expect(first.ok, JSON.stringify(first)).toBe(true);
const manaAfterFirst = useCombatStore.getState().resource;
const clearcasting = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
));
expect(clearcasting?.expiresAt).toBe(start + 15_000);
expect(useCombatStore.getState().cooldowns["wow335-druid-omen-of-clarity"]).toBe(start + 2_000);
const second = useCombatStore.getState().castAbility(moonfire!.id, {
targetActorId: TARGET_ID,
now: start + 1_500,
});
expect(second.ok, JSON.stringify(second)).toBe(true);
expect(useCombatStore.getState().resource).toBe(manaAfterFirst);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
))).toBe(false);
});
it("allows a successful healing spell that fully overheals to proc Omen of Clarity", () => {
const healingTouch = abilityId("Healing Touch");
useCombatStore.setState((state) => ({
health: state.maxHealth,
talentRanks: { ...state.talentRanks, "talent-827": 3 },
}));
let startedAt = 1_000;
while (unit(`${PLAYER_AGGRO_ID}:${healingTouch}:${PLAYER_AURA_ENTITY_ID}:${startedAt + 3_000}:heal:druid-omen-of-clarity`) >= 0.06) {
startedAt += 100;
}
const result = useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt });
expect(result.ok, JSON.stringify(result)).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
expect(activeCast?.abilityId).toBe(healingTouch);
useCombatStore.getState().tick(0.1, activeCast!.completesAt);
const healingEvent = useCombatStore.getState().combatEvents.find((event) => (
event.kind === "healing" && event.abilityId === healingTouch && event.occurredAt === activeCast!.completesAt
));
expect(healingEvent).toMatchObject({ rawAmount: expect.any(Number), effectiveAmount: 0 });
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
))).toBe(true);
});
it("allows landed player auto attacks to trigger Omen of Clarity", () => {
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-827": 3 } }));
let now = 1_000;
let seed = `druid-auto-attack:${now}`;
while (unit(`${seed}:druid-omen-of-clarity`) >= 0.06) {
now += 1;
seed = `druid-auto-attack:${now}`;
}
const result = useCombatStore.getState().damageMob(TARGET_ID, 100, now, undefined, "physical", {
attackKind: "melee",
seed,
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
});
expect(result.amount).toBeGreaterThan(0);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
))).toBe(true);
});
it("stores 30% of an eligible critical heal and blooms on the target's next direct hit", () => {
const healingTouch = abilityId("Healing Touch");
const startedAt = 1_000;
useCombatStore.setState((state) => ({
health: 1,
talentRanks: { ...state.talentRanks, "talent-1922": 3 },
auras: [...applyAura(state.auras, {
definition: {
id: "test-druid-guaranteed-spell-crit",
name: "Test Critical Chance",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: startedAt,
}).auras],
}));
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt }).ok).toBe(true);
const cast = useCombatStore.getState().activeCast;
expect(cast?.abilityId).toBe(healingTouch);
useCombatStore.getState().tick(0.1, cast!.completesAt);
const heal = useCombatStore.getState().combatEvents.find((event) => (
event.kind === "healing" && event.abilityId === healingTouch && event.occurredAt === cast!.completesAt
));
expect(heal).toMatchObject({ critical: true, effectiveAmount: expect.any(Number) });
const storedSeed = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
));
expect(storedSeed?.expiresAt).toBe(cast!.completesAt + 15_000);
expect(storedSeed?.druidLivingSeedAmount).toBe(Math.round(heal!.effectiveAmount * 0.3));
const healthBeforeHit = useCombatStore.getState().health;
const damage = useCombatStore.getState().damagePlayer(100, "physical", 80, cast!.completesAt + 1);
expect(damage).toBeGreaterThan(0);
expect(useCombatStore.getState().health).toBeGreaterThan(healthBeforeHit - damage);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
))).toBe(false);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "healing" && event.abilityId === "wow335:druid:living-seed" && event.effectiveAmount > 0
))).toBe(true);
});
it("expires an untriggered Living Seed after fifteen seconds", () => {
const healingTouch = abilityId("Healing Touch");
const startedAt = 1_000;
useCombatStore.setState((state) => ({
health: 1,
talentRanks: { ...state.talentRanks, "talent-1922": 3 },
auras: [...applyAura(state.auras, {
definition: {
id: "test-druid-guaranteed-spell-crit",
name: "Test Critical Chance",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: startedAt,
}).auras],
}));
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt }).ok).toBe(true);
const cast = useCombatStore.getState().activeCast;
useCombatStore.getState().tick(0.1, cast!.completesAt);
const expiresAt = cast!.completesAt + 15_000;
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
))).toBe(true);
const healthBeforeHit = useCombatStore.getState().health;
const damage = useCombatStore.getState().damagePlayer(100, "physical", 80, expiresAt);
expect(damage).toBeGreaterThan(0);
expect(useCombatStore.getState().health).toBe(healthBeforeHit - damage);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
))).toBe(false);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "healing" && event.abilityId === "wow335:druid:living-seed"
))).toBe(false);
});
it("blooms on a landed direct enemy hit to a party ally", () => {
const member = usePartyStore.getState().members[0]!;
const memberMaxHealth = 1_000;
usePartyStore.setState((state) => ({
members: state.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: 100, maxHealth: memberMaxHealth }
: candidate),
}));
useCombatStore.setState((state) => ({
actors: {
...state.actors,
[member.id]: { ...state.actors[member.id]!, health: 100, maxHealth: memberMaxHealth, alive: true },
},
talentRanks: { ...state.talentRanks, "talent-1922": 3 },
auras: [...applyAura(state.auras, {
definition: {
id: "test-druid-guaranteed-spell-crit",
name: "Test Critical Chance",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras,
...applyAura(state.auras, {
definition: {
id: "test-druid-guaranteed-ally-hit",
name: "Test Incoming Hit Chance",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: 100 }],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: member.id,
now: 1_000,
}).auras],
}));
registerPartyRuntimePosition(member.id, [2, 0, 0]);
const healingTouch = abilityId("Healing Touch");
useCombatStore.getState().synchronizeActors();
useCombatStore.getState().selectFriendlyActor(member.id);
const cast = useCombatStore.getState().castAbility(healingTouch, { targetActorId: member.id, now: 1_000 });
expect(cast.ok, JSON.stringify(cast)).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
useCombatStore.getState().tick(0.1, activeCast!.completesAt);
const storedSeed = useCombatStore.getState().auras.find((aura) => (
aura.targetId === member.id
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
));
expect(storedSeed?.druidLivingSeedAmount).toBeGreaterThan(0);
const mobId = "druid-living-seed-party-hit";
const attackId = "druid-living-seed-party-swing";
useCombatStore.getState().registerMob(mobId, {
name: "Party attacker",
level: 1,
maxHealth: 1_000,
xpReward: 0,
attacks: [{
...DEFAULT_ENEMY_ATTACK,
id: attackId,
initialCooldownMs: [0, 0],
repeatCooldownMs: [1_000, 1_000],
}],
});
setMobPosition(mobId, [4, 0, 0]);
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
[mobId]: {
...state.mobs[mobId]!,
engaged: true,
combatPhase: "attacking",
targetActorId: member.id,
threatByActor: { [member.id]: 100 },
},
},
}));
useCombatStore.getState().advanceMobCombat(activeCast!.completesAt + 500);
const enemyDamage = useCombatStore.getState().combatEvents.find((event) => (
event.kind === "damage" && event.sourceActorId === mobId && event.targetActorId === member.id
));
expect(enemyDamage).toMatchObject({ outcome: "hit" });
const remainingSeed = useCombatStore.getState().auras.find((aura) => (
aura.targetId === member.id
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
));
expect(remainingSeed).toBeUndefined();
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "healing" && event.abilityId === "wow335:druid:living-seed"
&& event.targetActorId === member.id && event.effectiveAmount > 0
))).toBe(true);
});
it("grants five Rage on Bear and Dire Bear Form critical auto-attacks at Primal Fury's 50% rank", () => {
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-801": 1 },
resourcePools: { ...state.resourcePools, rage: 0 },
maxResourcePools: { ...state.maxResourcePools, rage: 100 },
}));
guaranteeDruidMeleeCrits();
const form = useCombatStore.getState().castAbility(abilityId("Bear Form"), {
targetActorId: null,
now: 1_000,
});
expect(form.ok, JSON.stringify(form)).toBe(true);
const procSeed = findDruidPrimalFurySeed("rage", 0.5, true);
const proc = useCombatStore.getState().damageMob(TARGET_ID, 100, 2_000, undefined, "physical", {
attackKind: "melee",
seed: procSeed,
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
});
expect(proc.combatResult?.critical).toBe(true);
expect(useCombatStore.getState().resourcePools.rage).toBe(5);
expect(useCombatStore.getState().combatEvents.at(-1)).toMatchObject({
kind: "resource",
sourceActorId: PLAYER_AGGRO_ID,
targetActorId: PLAYER_AURA_ENTITY_ID,
abilityId: "wow335-druid-primal-fury",
rawAmount: 5,
effectiveAmount: 5,
});
const noProcSeed = findDruidPrimalFurySeed("rage", 0.5, false);
const noProc = useCombatStore.getState().damageMob(TARGET_ID, 100, 3_000, undefined, "physical", {
attackKind: "melee",
seed: noProcSeed,
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
});
expect(noProc.combatResult?.critical).toBe(true);
expect(useCombatStore.getState().resourcePools.rage).toBe(5);
const direBearForm = useCombatStore.getState().castAbility(abilityId("Dire Bear Form"), {
targetActorId: null,
now: 4_000,
});
expect(direBearForm.ok, JSON.stringify(direBearForm)).toBe(true);
const direBearProc = useCombatStore.getState().damageMob(TARGET_ID, 100, 5_000, undefined, "physical", {
attackKind: "melee",
seed: findDruidPrimalFurySeed("rage", 0.5, true),
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
});
expect(direBearProc.combatResult?.critical).toBe(true);
expect(useCombatStore.getState().resourcePools.rage).toBe(10);
});
it("adds one capped combo point on a Cat Form combo-builder critical and ignores other hits", () => {
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-801": 2 },
resourcePools: { ...state.resourcePools, rage: 0 },
maxResourcePools: { ...state.maxResourcePools, rage: 100 },
}));
guaranteeDruidMeleeCrits();
const form = useCombatStore.getState().castAbility(abilityId("Cat Form"), {
targetActorId: null,
now: 1_000,
});
expect(form.ok, JSON.stringify(form)).toBe(true);
const claw = castAndFinish("Claw", 3_000);
expect(claw).toMatchObject({ critical: true, sourceActorId: PLAYER_AGGRO_ID, targetActorId: TARGET_ID });
expect(useCombatStore.getState().comboPoints).toBe(2);
expect(useCombatStore.getState().comboTargetId).toBe(TARGET_ID);
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
useCombatStore.setState((state) => ({
comboPoints: 4,
comboTargetId: TARGET_ID,
resourcePools: {
...state.resourcePools,
energy: state.maxResourcePools.energy,
},
}));
const cappedClaw = castAndFinish("Claw", 5_000);
expect(cappedClaw?.critical).toBe(true);
expect(useCombatStore.getState().comboPoints).toBe(5);
expect(useCombatStore.getState().comboTargetId).toBe(TARGET_ID);
useCombatStore.setState((state) => ({ comboPoints: 0, comboTargetId: null }));
const clawId = abilityId("Claw");
useCombatStore.getState().damageMob(TARGET_ID, 100, 7_000, undefined, "physical", {
attackKind: "melee",
seed: "druid-primal-fury-cat-auto-attack",
abilityId: clawId,
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
});
expect(useCombatStore.getState().comboPoints).toBe(0);
expect(useCombatStore.getState().comboTargetId).toBeNull();
useCombatStore.setState((state) => ({
comboPoints: 0,
comboTargetId: null,
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy },
}));
const rake = castAndFinish("Rake", 8_000);
expect(rake).toMatchObject({ critical: true, sourceActorId: PLAYER_AGGRO_ID, targetActorId: TARGET_ID });
expect(useCombatStore.getState().comboPoints).toBe(2);
useCombatStore.getState().tick(9, 17_000);
expect(useCombatStore.getState().comboPoints).toBe(2);
expect(useCombatStore.getState().comboTargetId).toBe(TARGET_ID);
useCombatStore.setState((state) => ({ comboPoints: 0, comboTargetId: null }));
useCombatStore.getState().damageMob(TARGET_ID, 100, 18_000, undefined, "arcane", {
attackKind: "spell",
seed: "druid-primal-fury-cat-non-combo",
abilityId: abilityId("Moonfire"),
rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false },
});
expect(useCombatStore.getState().comboPoints).toBe(0);
expect(useCombatStore.getState().comboTargetId).toBeNull();
});
});