forked from phenom/healer-man
602 lines
26 KiB
TypeScript
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();
|
|
});
|
|
});
|