Update Healer Man server

This commit is contained in:
phenom committed 2026-09-23 16:14:19 -04:00
1 parent f90ba7d989
commit 189b94d6b7
51 files changed
+3448 -154

No files matched your search

+343 -1
View File
@@ -19,9 +19,11 @@ import {
} from "./combatStore";
import { TALENT_NODES, talentNodesForTree } from "./talentCatalog";
import { EMPTY_ITEM_STATS } from "./itemStats";
import { abilitiesForClass, abilityAtLevel, defaultActionBarForClass } from "./abilityCatalog";
import { abilitiesForClass, abilityAtLevel, abilityTalentNodeId, defaultActionBarForClass } from "./abilityCatalog";
import { defaultActionBindings } from "./actionBindings";
import { deriveCharacterStats, MANA_POOL_MULTIPLIER } from "./wotlkStats";
import { applyAura as applyCombatAura } from "./combatAuras";
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
import { loadCanonicalBenchmarkBuilds } from "./benchmark/benchmarkBuilds";
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
@@ -50,6 +52,13 @@ function registerTarget(
useCombatStore.getState().selectMob(id);
}
function unlockReadySpell(id: string): void {
const ability = useCombatStore.getState().abilities.find((entry) => entry.id === id);
expect(ability, id).toBeDefined();
const nodeId = abilityTalentNodeId(ability!);
if (nodeId) useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, [nodeId]: 1 } }));
}
function setPartyMemberHealth(id: string, health: number): void {
usePartyStore.setState((state) => ({
members: state.members.map((member) => member.id === id
@@ -198,6 +207,34 @@ describe("combat store", () => {
unsubscribe();
});
it("damages nearby enemies and heals nearby allies with one Holy Nova cast", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
registerTarget("nova-near", [3, 0, 0], 100_000, 0);
useCombatStore.getState().registerMob("nova-far", { name: "nova-far", maxHealth: 100_000, xpReward: 0 });
setMobPosition("nova-far", [15, 0, 0]);
const nearbyAlly = preparePartyMember(0, [3, 0, 0], 1);
const start = useCombatStore.getState();
useCombatStore.setState({ health: start.maxHealth - 1_000 });
const events: CombatPresentationEvent[] = [];
const unsubscribe = subscribeCombatPresentation((message) => {
if (message.kind === "event" && message.event.abilityId === "wow335-priest-holy-nova") {
events.push(message.event);
}
});
const result = useCombatStore.getState().castAbility("wow335-priest-holy-nova", [0, 0, 0], 1_000);
unsubscribe();
expect(result).toMatchObject({ ok: true, affectedMobIds: ["nova-near"] });
expect(result.damage).toBeGreaterThan(0);
expect(result.healing).toBeGreaterThan(0);
expect(useCombatStore.getState().mobs["nova-near"].health).toBeLessThan(100_000);
expect(useCombatStore.getState().mobs["nova-far"].health).toBe(100_000);
expect(useCombatStore.getState().health).toBeGreaterThan(start.maxHealth - 1_000);
expect(usePartyStore.getState().members.find((member) => member.id === nearbyAlly)?.health).toBeGreaterThan(1);
expect(events.map((event) => event.phase)).toEqual(["release", "impact"]);
});
it("identifies the player as the target of a self-cast aura presentation", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
const events: CombatPresentationEvent[] = [];
@@ -1734,4 +1771,309 @@ describe("combat store", () => {
expect(useCombatStore.getState().mobs["storm-boss"].pendingLoot).toHaveLength(0);
expect(useCombatStore.getState().mobs["storm-boss"].bossCardDrop?.rarities).toContain("normal");
});
it("stacks Sunder through Devastate and grants Last Stand temporary maximum health", () => {
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80 });
unlockReadySpell("wow335-warrior-devastate");
unlockReadySpell("wow335-warrior-last-stand");
useCombatStore.setState((state) => ({ resource: 100, resourcePools: { ...state.resourcePools, rage: 100 } }));
registerTarget("sunder-target", [2, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-warrior-devastate", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().auras.find((aura) => aura.targetId === "sunder-target"
&& aura.definition.id === "wow335:warrior:sunder-armor")?.stacks).toBe(1);
expect(useCombatStore.getState().castAbility("wow335-warrior-devastate", [0, 0, 0], 3_000).ok).toBe(true);
expect(useCombatStore.getState().auras.find((aura) => aura.targetId === "sunder-target"
&& aura.definition.id === "wow335:warrior:sunder-armor")?.stacks).toBe(2);
const before = useCombatStore.getState().maxHealth;
expect(useCombatStore.getState().castAbility("wow335-warrior-last-stand", [0, 0, 0], 5_000).ok).toBe(true);
expect(useCombatStore.getState().maxHealth).toBeGreaterThan(before * 1.29);
useCombatStore.getState().tick(0, 24_999);
expect(useCombatStore.getState().maxHealth).toBeGreaterThan(before * 1.29);
useCombatStore.getState().tick(0, 26_000);
expect(useCombatStore.getState().maxHealth).toBe(before);
});
it("requires and consumes Enrage for Enraged Regeneration", () => {
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80 });
unlockReadySpell("wow335-warrior-enraged-regeneration");
useCombatStore.setState((state) => ({ resource: 100, resourcePools: { ...state.resourcePools, rage: 100 } }));
expect(useCombatStore.getState().castAbility("wow335-warrior-enraged-regeneration", [0, 0, 0], 1_000))
.toMatchObject({ ok: false, reason: "condition-not-met" });
const enrage = applyCombatAura(useCombatStore.getState().auras, {
definition: { id: "test:enrage", name: "Enrage", disposition: "buff", dispelCategory: "enrage", durationMs: 12_000 },
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
});
useCombatStore.setState({ auras: enrage.auras });
expect(useCombatStore.getState().castAbility("wow335-warrior-enraged-regeneration", [0, 0, 0], 2_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "test:enrage")).toBe(false);
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === "wow335-warrior-enraged-regeneration")).toBe(true);
});
it("prevents recasting Power Word: Shield until Weakened Soul expires", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
expect(useCombatStore.getState().castAbility("wow335-priest-power-word-shield", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === "wow335:priest:weakened-soul")?.expiresAt)
.toBe(31_000);
expect(useCombatStore.getState().castAbility("wow335-priest-power-word-shield", [0, 0, 0], 10_000))
.toMatchObject({ ok: false, reason: "condition-not-met" });
useCombatStore.getState().tick(0, 31_000);
expect(useCombatStore.getState().castAbility("wow335-priest-power-word-shield", [0, 0, 0], 31_000).ok).toBe(true);
});
it("grants Inner Fire armor for 20 landed physical hits", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
expect(useCombatStore.getState().castAbility("wow335-priest-inner-fire", [0, 0, 0], 1_000).ok).toBe(true);
const charges = () => useCombatStore.getState().auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.name.startsWith("Inner Fire (DBC ") && aura.definition.modifiers?.some((modifier) => (
modifier.kind === "stat" && modifier.stat === "armor"
)))?.stacks;
expect(charges()).toBe(20);
registerTarget("inner-fire-attacker", [2, 0, 0], 100_000, 0);
useCombatStore.getState().damageMob("inner-fire-attacker", 1, 1_000);
for (let now = 1_450; now < 15_000 && charges() === 20; now += 1_900) {
useCombatStore.getState().advanceMobCombat(now);
}
expect(charges()).toBe(19);
expect(useCombatStore.getState().lastCombatResult?.effectiveArmor).toBeGreaterThan(2_000);
});
it("makes Evasion reduce incoming ranged hit chance", () => {
useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 80 });
expect(useCombatStore.getState().castAbility("wow335-rogue-evasion", [0, 0, 0], 1_000).ok).toBe(true);
useCombatStore.getState().registerMob("evasion-attacker", {
name: "Evasion attacker", level: 80, maxHealth: 100_000, xpReward: 0,
attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "evasion-arrow", delivery: "projectile", range: 30 }],
});
setMobPosition("evasion-attacker", [8, 0, 0]);
useCombatStore.getState().damageMob("evasion-attacker", 1, 1_000);
useCombatStore.getState().advanceMobCombat(1_450);
expect(useCombatStore.getState().lastCombatResult?.attackTable.missPercent).toBe(30);
});
it("summons one Shadowfiend and restores mana only after its attacks", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
registerTarget("fiend-target", [3, 0, 0], 100_000, 0);
useCombatStore.setState((state) => ({
resource: Math.round(state.maxResource / 2),
resourcePools: { ...state.resourcePools, mana: Math.round(state.maxResource / 2) },
}));
expect(useCombatStore.getState().castAbility("wow335-priest-shadowfiend", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().summons.filter((summon) => summon.sourceAbilityId === "wow335-priest-shadowfiend"))
.toHaveLength(1);
const manaBeforeAttack = useCombatStore.getState().resource;
useCombatStore.getState().tick(1, 2_000);
expect(useCombatStore.getState().mobs["fiend-target"].health).toBeLessThan(100_000);
expect(useCombatStore.getState().resource).toBeGreaterThanOrEqual(manaBeforeAttack + Math.floor(useCombatStore.getState().maxResource * 0.01));
});
it("casts Kill Shot only below 20 percent health and resolves one direct hit", () => {
useCombatStore.getState().initializeCharacter({ classId: "hunter", level: 80 });
registerTarget("kill-shot-target", [10, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-hunter-kill-shot", [0, 0, 0], 1_000))
.toMatchObject({ ok: false, reason: "condition-not-met" });
useCombatStore.setState((state) => ({ mobs: {
...state.mobs,
"kill-shot-target": { ...state.mobs["kill-shot-target"], health: 20_000 },
} }));
expect(useCombatStore.getState().castAbility("wow335-hunter-kill-shot", [0, 0, 0], 3_000).ok).toBe(true);
expect(useCombatStore.getState().combatEvents.filter((event) => event.kind === "damage"
&& event.abilityId === "wow335-hunter-kill-shot")).toHaveLength(1);
});
it("summons three Mirror Images and gives Combustion Fire hit stacks", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
unlockReadySpell("wow335-mage-combustion");
registerTarget("fire-target", [8, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-mage-mirror-image", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().summons.filter((summon) => summon.sourceAbilityId === "wow335-mage-mirror-image"))
.toHaveLength(3);
expect(useCombatStore.getState().castAbility("wow335-mage-combustion", [0, 0, 0], 3_000).ok).toBe(true);
expect(useCombatStore.getState().mobs["fire-target"].health).toBe(100_000);
const combustionId = "wow335:mage:combustion";
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === combustionId)?.stacks).toBe(1);
const fireBlast = useCombatStore.getState().abilities.find((ability) => ability.name === "Fire Blast")!;
expect(useCombatStore.getState().castAbility(fireBlast.id, [0, 0, 0], 5_000).ok).toBe(true);
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === combustionId)?.stacks).toBe(2);
useCombatStore.setState((state) => ({ auras: state.auras.map((aura) => aura.definition.id === combustionId
? { ...aura, stacks: 100, combustionCriticalHits: 2 }
: aura) }));
expect(useCombatStore.getState().castAbility(fireBlast.id, [0, 0, 0], 14_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === combustionId)).toBe(false);
});
it("scales Rupture duration and ticks with combo points", () => {
useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 80 });
registerTarget("rupture-target", [2, 0, 0], 100_000, 0);
useCombatStore.setState({ comboPoints: 1, comboTargetId: "rupture-target" });
expect(useCombatStore.getState().castAbility("wow335-rogue-rupture", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === "wow335-rogue-rupture")?.remainingTicks)
.toBe(4);
const firstAura = useCombatStore.getState().auras.find((aura) => aura.targetId === "rupture-target"
&& aura.definition.name.startsWith("Rupture"));
expect(firstAura?.expiresAt).toBe(9_000);
useCombatStore.setState({ comboPoints: 5, comboTargetId: "rupture-target", resource: 100 });
expect(useCombatStore.getState().castAbility("wow335-rogue-rupture", [0, 0, 0], 3_000).ok).toBe(true);
expect(useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === "wow335-rogue-rupture")?.remainingTicks)
.toBe(8);
expect(useCombatStore.getState().auras.find((aura) => aura.targetId === "rupture-target"
&& aura.definition.name.startsWith("Rupture"))?.expiresAt).toBe(19_000);
});
it("lets Penance damage an enemy and continue pulsing", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
unlockReadySpell("wow335-priest-penance");
registerTarget("penance-target", [8, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-priest-penance", [0, 0, 0], 1_000).ok).toBe(true);
const first = useCombatStore.getState().mobs["penance-target"].health;
expect(first).toBeLessThan(100_000);
useCombatStore.getState().tick(0, 2_000);
expect(useCombatStore.getState().mobs["penance-target"].health).toBeLessThan(first);
useCombatStore.getState().tick(0, 3_000);
expect(useCombatStore.getState().effects.some((effect) => effect.sourceAbilityId === "wow335-priest-penance")).toBe(false);
});
it("lets Penance heal a friendly target immediately and twice afterward", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
unlockReadySpell("wow335-priest-penance");
usePartyStore.getState().selectSelf();
useCombatStore.getState().damagePlayer(2_000);
const before = useCombatStore.getState().health;
expect(useCombatStore.getState().castAbility("wow335-priest-penance", [0, 0, 0], 1_000).ok).toBe(true);
const first = useCombatStore.getState().health;
expect(first).toBeGreaterThan(before);
useCombatStore.getState().tick(0, 2_000);
expect(useCombatStore.getState().health).toBeGreaterThan(first);
useCombatStore.getState().tick(0, 3_000);
expect(useCombatStore.getState().effects.some((effect) => effect.sourceAbilityId === "wow335-priest-penance")).toBe(false);
});
it("selects only the three lowest-health nearby Divine Hymn targets per two-second pulse", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
usePartyStore.getState().initializeParty("hymn-cap-test", 80, useCombatStore.getState().health);
const ids = [0, 1, 2, 3].map((index) => preparePartyMember(index,
[4 + index * 2, 0, 0], index + 1));
const before = usePartyStore.getState().members
.filter((member) => ids.includes(member.id))
.map((member) => ({ id: member.id, health: member.health, fraction: member.health / member.maxHealth }))
.sort((left, right) => left.fraction - right.fraction || left.id.localeCompare(right.id));
expect(useCombatStore.getState().castAbility("wow335-priest-divine-hymn", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().activeCast?.totalTicks).toBe(4);
useCombatStore.getState().tick(0, 3_000);
const members = usePartyStore.getState().members;
expect(before.slice(0, 3).every((entry) => members.find((member) => member.id === entry.id)!.health > entry.health)).toBe(true);
expect(members.find((member) => member.id === before[3].id)!.health).toBe(before[3].health);
});
it("refreshes Serpent Sting and triggers its Chimera Shot damage", () => {
useCombatStore.getState().initializeCharacter({ classId: "hunter", level: 80 });
unlockReadySpell("wow335-hunter-chimera-shot");
registerTarget("sting-target", [10, 0, 0], 100_000, 0);
const stingId = useCombatStore.getState().abilities.find((ability) => ability.name === "Serpent Sting")!.id;
expect(useCombatStore.getState().castAbility(stingId, [0, 0, 0], 1_000).ok).toBe(true);
const dotBefore = useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === stingId);
expect(dotBefore).toBeDefined();
expect(useCombatStore.getState().castAbility("wow335-hunter-chimera-shot", [0, 0, 0], 3_000).ok).toBe(true);
const dotAfter = useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === stingId);
expect(dotAfter?.nextTickAt).toBe(3_000 + dotBefore!.intervalMs);
expect(dotAfter?.remainingTicks).toBeGreaterThanOrEqual(dotBefore!.remainingTicks);
expect(useCombatStore.getState().combatEvents.filter((event) => event.abilityId === "wow335-hunter-chimera-shot"
&& event.kind === "damage")).toHaveLength(2);
});
it("restores mana from Viper Sting and limits Scorpid Sting disarm to once per minute", () => {
useCombatStore.getState().initializeCharacter({ classId: "hunter", level: 80 });
registerTarget("sting-utility-target", [10, 0, 0], 100_000, 0);
const viperId = useCombatStore.getState().abilities.find((ability) => ability.name === "Viper Sting")!.id;
expect(useCombatStore.getState().castAbility(viperId, [0, 0, 0], 1_000).ok).toBe(true);
useCombatStore.setState((state) => ({
resource: state.maxResource / 2,
resourcePools: { ...state.resourcePools, mana: state.maxResource / 2 },
}));
const manaBefore = useCombatStore.getState().resource;
useCombatStore.getState().triggerChimeraShot(PLAYER_AURA_ENTITY_ID, "sting-utility-target", "wow335-hunter-chimera-shot", 2_000);
expect(useCombatStore.getState().resource).toBeGreaterThan(manaBefore);
useCombatStore.getState().initializeCharacter({ classId: "hunter", level: 80 });
registerTarget("scorpid-target", [10, 0, 0], 100_000, 0);
const scorpidId = useCombatStore.getState().abilities.find((ability) => ability.name === "Scorpid Sting")!.id;
expect(useCombatStore.getState().castAbility(scorpidId, [0, 0, 0], 1_000).ok).toBe(true);
useCombatStore.getState().triggerChimeraShot(PLAYER_AURA_ENTITY_ID, "scorpid-target", "wow335-hunter-chimera-shot", 2_000);
const disarm = useCombatStore.getState().mobs["scorpid-target"].statuses.find((status) => status.kind === "disarm");
expect(disarm?.endsAt).toBe(12_000);
useCombatStore.getState().triggerChimeraShot(PLAYER_AURA_ENTITY_ID, "scorpid-target", "wow335-hunter-chimera-shot", 3_000);
expect(useCombatStore.getState().mobs["scorpid-target"].statuses.find((status) => status.kind === "disarm")?.endsAt).toBe(12_000);
});
it("explodes Living Bomb onto nearby enemies when its last tick completes", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
unlockReadySpell("wow335-mage-living-bomb");
registerTarget("bomb-target", [5, 0, 0], 100_000, 0);
useCombatStore.getState().registerMob("bomb-neighbor", { name: "bomb-neighbor", maxHealth: 100_000, xpReward: 0 });
setMobPosition("bomb-neighbor", [7, 0, 0]);
expect(useCombatStore.getState().castAbility("wow335-mage-living-bomb", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().mobs["bomb-neighbor"].health).toBe(100_000);
useCombatStore.getState().tick(0, 13_000);
expect(useCombatStore.getState().mobs["bomb-neighbor"].health).toBeLessThan(100_000);
});
it("detonates Living Bomb once when its magic aura is dispelled", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
unlockReadySpell("wow335-mage-living-bomb");
registerTarget("dispel-bomb", [5, 0, 0], 100_000, 0);
useCombatStore.getState().registerMob("dispel-neighbor", { name: "dispel-neighbor", maxHealth: 100_000, xpReward: 0 });
setMobPosition("dispel-neighbor", [7, 0, 0]);
expect(useCombatStore.getState().castAbility("wow335-mage-living-bomb", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().dispelActor("dispel-bomb", ["magic"], 1, 2_000)).toBe(1);
expect(useCombatStore.getState().mobs["dispel-neighbor"].health).toBeLessThan(100_000);
expect(useCombatStore.getState().effects.some((effect) => effect.targetId === "dispel-bomb"
&& effect.sourceAbilityId === "wow335-mage-living-bomb")).toBe(false);
});
it("casts Molten Armor on self without initial damage", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
registerTarget("armor-target", [8, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-mage-molten-armor", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().mobs["armor-target"].health).toBe(100_000);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.name.startsWith("Molten Armor"))).toBe(true);
});
it("applies Molten Armor's Spirit crit conversion and incoming crit reduction", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
registerTarget("molten-crit-target", [8, 0, 0], 100_000, 0);
const fireBlast = useCombatStore.getState().abilities.find((ability) => ability.name === "Fire Blast")!;
expect(useCombatStore.getState().castAbility(fireBlast.id, [0, 0, 0], 3_000).ok).toBe(true);
const baseCrit = useCombatStore.getState().lastCombatResult!.attackTable.criticalPercent;
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 });
registerTarget("molten-crit-target", [8, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-mage-molten-armor", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().castAbility(fireBlast.id, [0, 0, 0], 3_000).ok).toBe(true);
expect(useCombatStore.getState().lastCombatResult!.attackTable.criticalPercent).toBeGreaterThan(baseCrit);
useCombatStore.getState().registerMob("molten-attacker", {
name: "Molten attacker", level: 80, maxHealth: 100_000, xpReward: 0,
attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "molten-arrow", delivery: "projectile", range: 30 }],
});
setMobPosition("molten-attacker", [8, 0, 0]);
useCombatStore.getState().damageMob("molten-attacker", 1, 3_000);
useCombatStore.getState().advanceMobCombat(3_450);
expect(useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent).toBe(0);
});
it("awards Shiv's combo point and consumes Cold Blood on the next strike", () => {
useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 80 });
unlockReadySpell("wow335-rogue-cold-blood");
registerTarget("rogue-target", [2, 0, 0], 100_000, 0);
expect(useCombatStore.getState().castAbility("wow335-rogue-shiv", [0, 0, 0], 1_000).ok).toBe(true);
expect(useCombatStore.getState().comboPoints).toBe(1);
expect(useCombatStore.getState().castAbility("wow335-rogue-cold-blood", [0, 0, 0], 3_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("next-offensive-critical"))).toBe(true);
expect(useCombatStore.getState().castAbility("rogue-sinister-strike", [0, 0, 0], 5_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("next-offensive-critical"))).toBe(false);
expect(useCombatStore.getState().lastCombatResult?.outcome).toBe("crit");
});
});