import { beforeEach, describe, expect, it } from "vitest"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimePosition, type PartyWorldPosition, } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; import { DEFAULT_GAMEPLAY_SETTINGS, EMPTY_CORPSE_DESPAWN_MS, MINIMUM_CORPSE_VISIBLE_MS, MOB_WOUND_ANIMATION_COOLDOWN_MS, selectedMob, useCombatStore, } from "./combatStore"; import { TALENT_NODES, talentNodesForTree } from "./talentCatalog"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { abilitiesForClass, defaultActionBarForClass } from "./abilityCatalog"; import { defaultActionBindings } from "./actionBindings"; import { deriveCharacterStats } from "./wotlkStats"; import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation"; function allocateTalentRanks(name: string, ranks: number): void { const talent = TALENT_NODES.find((node) => node.name === name); if (!talent) throw new Error(`Missing test talent: ${name}`); for (let rank = 0; rank < ranks; rank += 1) { expect(useCombatStore.getState().allocateTalent(talent.id)).toBe(true); } } function registerTarget( id = "target", position: readonly [number, number, number] = [10, 0, 0], maxHealth = 100, xpReward = 20, ) { useCombatStore.getState().registerMob(id, { name: id, maxHealth, xpReward }); setMobPosition(id, position); useCombatStore.getState().selectMob(id); } function setPartyMemberHealth(id: string, health: number): void { usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === id ? { ...member, health: Math.max(0, Math.min(member.maxHealth, health)) } : member), })); } function preparePartyMember( index: number, position: PartyWorldPosition, health = 1, select = false, ): string { const member = usePartyStore.getState().members[index]; if (!member) throw new Error(`Missing party member ${index}`); setPartyMemberHealth(member.id, health); registerPartyRuntimePosition(member.id, position); if (select) usePartyStore.getState().selectMember(member.id); return member.id; } describe("combat store", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 1, xp: 0 }); useCombatStore.getState().resetSettings(); useCombatStore.getState().setPlayerPosition([0, 0, 0]); usePartyStore.getState().initializeParty("combat-store-tests", 1, useCombatStore.getState().health); }); it("completes a ranged cast after its exact WoW 3.3.5a cast time", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 }); const startingResource = useCombatStore.getState().maxResource; registerTarget("ravager", [10, 0, 0]); const first = useCombatStore.getState().castSlot(0, undefined, 1000); expect(first).toMatchObject({ ok: true, abilityId: "mage-frostbolt", damage: 0 }); expect(useCombatStore.getState().mobs.ravager.health).toBe(100); expect(startingResource).toBe(165); expect(useCombatStore.getState().resource).toBe(startingResource); expect(useCombatStore.getState().activeCast).toMatchObject({ completesAt: 2500, mode: "cast" }); const second = useCombatStore.getState().castSlot(1, undefined, 1001); expect(second).toMatchObject({ ok: false, reason: "already-casting" }); useCombatStore.getState().tick(0.1, 2499); expect(useCombatStore.getState().mobs.ravager.health).toBe(100); useCombatStore.getState().tick(0.1, 2500); expect(useCombatStore.getState().mobs.ravager.health).toBeLessThan(100); expect(useCombatStore.getState().resource).toBe(startingResource - 14); expect(useCombatStore.getState().activeCast).toBeNull(); expect(useCombatStore.getState().mobs.ravager.engaged).toBe(true); }); it("emits exactly one cast and release event and cancels presentation with the cast", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 }); registerTarget("presentation-target", [10, 0, 0]); const events: CombatPresentationEvent[] = []; const unsubscribe = subscribeCombatPresentation((message) => { if (message.kind === "event") events.push(message.event); }); expect(useCombatStore.getState().castAbility("mage-frostbolt", [0, 0, 0], 1_000).ok).toBe(true); useCombatStore.getState().tick(0.1, 2_500); expect(events.filter((event) => event.abilityId === "mage-frostbolt").map((event) => event.phase)) .toEqual(["cast-start", "release", "impact"]); expect(useCombatStore.getState().castAbility("mage-frostbolt", [0, 0, 0], 4_000).ok).toBe(true); expect(useCombatStore.getState().cancelCast()).toBe(true); expect(events.filter((event) => event.abilityId === "mage-frostbolt").map((event) => event.phase)) .toEqual(["cast-start", "release", "impact", "cast-start", "cast-cancel"]); unsubscribe(); }); it("does not cast or bind Priest Holy Concentration", () => { const abilityId = "wow335-priest-holy-concentration"; expect(useCombatStore.getState().castAbility(abilityId, undefined, 1_000)).toMatchObject({ ok: false, reason: "passive", }); expect(useCombatStore.getState().setActionBinding("primary", "face-bottom", abilityId)).toBe(false); }); it("starts with attribute-derived pools including racial and automatic class passives", () => { const gnomeMage = deriveCharacterStats("mage", 1, EMPTY_ITEM_STATS, "gnome"); useCombatStore.getState().initializeCharacter({ classId: "mage", raceId: "gnome", level: 1 }); expect(useCombatStore.getState()).toMatchObject({ health: gnomeMage.health, maxHealth: gnomeMage.health, resource: gnomeMage.mana, maxResource: gnomeMage.mana, }); const taurenWarrior = deriveCharacterStats("warrior", 1, EMPTY_ITEM_STATS, "tauren"); useCombatStore.getState().initializeCharacter({ classId: "warrior", raceId: "tauren", level: 1 }); expect(useCombatStore.getState()).toMatchObject({ health: taurenWarrior.health, maxHealth: taurenWarrior.health, resource: 0, maxResource: 100, }); const chronomancerBase = deriveCharacterStats("chronomancer", 1, EMPTY_ITEM_STATS, "human"); useCombatStore.getState().initializeCharacter({ classId: "chronomancer", level: 1 }); expect(useCombatStore.getState()).toMatchObject({ resource: Math.round(chronomancerBase.mana * 1.12), maxResource: Math.round(chronomancerBase.mana * 1.12), }); useCombatStore.getState().initializeCharacter({ classId: "barbarian", level: 10 }); expect(useCombatStore.getState()).toMatchObject({ resource: 200, maxResource: 200 }); const primalistBase = deriveCharacterStats("primalist", 1, EMPTY_ITEM_STATS, "human"); useCombatStore.getState().initializeCharacter({ classId: "primalist", level: 1 }); expect(useCombatStore.getState()).toMatchObject({ health: Math.round(primalistBase.health * 1.08), maxHealth: Math.round(primalistBase.health * 1.08), resource: 0, }); const sunClericBase = deriveCharacterStats("sun-cleric", 1, EMPTY_ITEM_STATS, "human"); useCombatStore.getState().initializeCharacter({ classId: "sun-cleric", level: 1 }); expect(useCombatStore.getState().maxResource).toBe(sunClericBase.mana); const necromancerBase = deriveCharacterStats("necromancer", 20, EMPTY_ITEM_STATS, "human"); useCombatStore.getState().initializeCharacter({ classId: "necromancer", level: 20 }); expect(useCombatStore.getState().maxResource).toBe(necromancerBase.mana); }); it("uses equipped spell power, stamina, and armor in player combat", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 }); const baseMaxHealth = useCombatStore.getState().maxHealth; const staff = { instanceId: "stat-staff", itemId: 6631, itemKey: "stat-staff", name: "Stat Staff", quality: "rare" as const, inventorySlot: "two-hand", armorType: null, weaponType: "staff", itemLevel: 1, sourceItemLevel: 25, stats: { ...EMPTY_ITEM_STATS, spellPower: 7, stamina: 3, armor: 100 }, quantity: 1, sourceBossKey: "test-boss", sourceBossName: "Test Boss", acquiredAt: 1, }; useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1, inventory: [staff], equipment: { player: { "main-hand": staff.instanceId } }, }); expect(useCombatStore.getState()).toMatchObject({ maxHealth: baseMaxHealth + 30, gearStats: staff.stats, }); registerTarget("geared-target", [10, 0, 0]); useCombatStore.getState().castSlot(0, undefined, 1_000); useCombatStore.getState().tick(0.1, 2_500); expect(useCombatStore.getState().mobs["geared-target"].health).toBeLessThan(100); expect(useCombatStore.getState().damagePlayer(100)).toBeLessThan(100); }); it("uses spell penetration against a mob's matching magical resistance", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20 }); useCombatStore.getState().registerMob("resisted-fire", { name: "Resisted Fire Target", level: 20, maxHealth: 1_000, xpReward: 0, resistances: { fire: 100 }, }); const resisted = useCombatStore.getState().damageMob( "resisted-fire", 100, 1_000, undefined, "fire", ).amount; useCombatStore.setState({ gearStats: { ...EMPTY_ITEM_STATS, spellPenetration: 100 }, }); useCombatStore.getState().registerMob("penetrated-fire", { name: "Penetrated Fire Target", level: 20, maxHealth: 1_000, xpReward: 0, resistances: { fire: 100 }, }); const penetrated = useCombatStore.getState().damageMob( "penetrated-fire", 100, 2_000, undefined, "fire", ).amount; expect(resisted).toBeLessThan(100); expect(penetrated).toBe(100); }); it("casts customized controller chords by stable ability id and holds on empty bindings", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 }); registerTarget("binding-target", [10, 0, 0]); expect(useCombatStore.getState().actionBindings["primary:face-bottom"]).toBe("mage-frostbolt"); expect(useCombatStore.getState().actionBindings["primary:face-right"]) .toBe(defaultActionBarForClass("mage")[3]); expect(useCombatStore.getState().setActionBinding("secondary", "r2", "mage-fire-blast")).toBe(true); expect(useCombatStore.getState().setActionBinding("secondary", "r1", "priest-flash-heal")).toBe(false); expect(useCombatStore.getState().castActionBinding("secondary", "r2", [0, 0, 0], 1_000)).toMatchObject({ ok: true, abilityId: "mage-fire-blast", }); expect(useCombatStore.getState().setActionBinding("tertiary", "r2", null)).toBe(true); expect(useCombatStore.getState().castActionBinding("tertiary", "r2", [0, 0, 0], 2_000)).toMatchObject({ ok: false, reason: "unknown-ability", }); expect(useCombatStore.getState().feedback?.message).toMatch(/binding is empty/i); }); it("normalizes saved bindings and can restore one layer or every layer", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", actionBindings: { "primary:face-bottom": null, "secondary:r1": "mage-fire-blast", "secondary:r2": "priest-flash-heal", }, }); expect(useCombatStore.getState().actionBindings["primary:face-bottom"]).toBeNull(); expect(useCombatStore.getState().actionBindings["secondary:r1"]).toBe("mage-fire-blast"); expect(useCombatStore.getState().actionBindings["secondary:r2"]).toBeNull(); useCombatStore.getState().resetActionBindingLayer("secondary"); const mageDefaults = defaultActionBindings(defaultActionBarForClass("mage")); expect(useCombatStore.getState().actionBindings["secondary:r1"]).toBe(mageDefaults["secondary:r1"]); expect(useCombatStore.getState().actionBindings["primary:face-bottom"]).toBeNull(); useCombatStore.getState().resetActionBindings(); expect(useCombatStore.getState().actionBindings["primary:face-bottom"]).toBe("mage-frostbolt"); }); it("grants the level-ten starter kit while retaining later ability gates", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1, xp: 0 }); expect(useCombatStore.getState().castAbility("mage-frost-nova", [0, 0, 0], 1_000).ok).toBe(true); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 19, xp: 0 }); expect(useCombatStore.getState().castAbility("mage-mana-shield", undefined, 2_000)).toMatchObject({ ok: false, reason: "level-locked", abilityId: "mage-mana-shield", }); expect(useCombatStore.getState().feedback?.message).toBe("That ability unlocks at level 20."); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20, xp: 0 }); expect(useCombatStore.getState().castAbility("mage-mana-shield", undefined, 3_000).ok).toBe(true); }); it("rejects unavailable and out-of-range targets without spending resources", () => { useCombatStore.getState().initializeCharacter({ classId: "priest" }); useCombatStore.getState().registerMob("hidden", { name: "Hidden" }); useCombatStore.getState().selectMob("hidden"); expect(useCombatStore.getState().castSlot(0, undefined, 1000).reason).toBe("target-unavailable"); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); setMobPosition("hidden", [60, 0, 0]); expect(useCombatStore.getState().castSlot(0, undefined, 1000).reason).toBe("out-of-range"); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); }); it("heals, shields, and applies shield absorption before health damage", () => { useCombatStore.getState().damagePlayer(40); const before = useCombatStore.getState().health; const heal = useCombatStore.getState().castAbility("wow335-priest-lesser-heal", undefined, 1000); expect(heal.ok).toBe(true); expect(useCombatStore.getState().health).toBe(before); useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt); expect(useCombatStore.getState().health).toBeGreaterThan(before); expect(useCombatStore.getState().health - before).toBeGreaterThanOrEqual(30); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20 }); expect(useCombatStore.getState().castAbility("mage-mana-shield", undefined, 5000).ok).toBe(true); const shield = useCombatStore.getState().shield; expect(shield).toBeGreaterThan(0); const healthBefore = useCombatStore.getState().health; const healthDamage = useCombatStore.getState().damagePlayer(shield + 7); expect(healthDamage).toBe(7); expect(useCombatStore.getState().health).toBe(healthBefore - 7); expect(useCombatStore.getState().shield).toBe(0); }); it("ticks damage over time and awards kill experience exactly once", () => { useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 1, xp: 0 }); registerTarget("ooze", [10, 0, 0], 5, 100); expect(useCombatStore.getState().castSlot(1, undefined, 1000).ok).toBe(true); useCombatStore.getState().tick(1, 4000); expect(useCombatStore.getState().mobs.ooze.health).toBeLessThan(5); useCombatStore.getState().tick(1, 7000); expect(useCombatStore.getState()).toMatchObject({ level: 2, xp: 0 }); expect(useCombatStore.getState().mobs.ooze.dead).toBe(true); useCombatStore.getState().tick(1, 30_000); expect(useCombatStore.getState()).toMatchObject({ level: 2, xp: 0 }); expect(useCombatStore.getState().effects).toHaveLength(0); }); it("supports multi-level rewards without duplicate death credit", () => { registerTarget("boss", [2, 0, 0], 1, 400); const first = useCombatStore.getState().damageMob("boss", 10, 1000); expect(first).toMatchObject({ killed: true, xpAwarded: 1_600, levelsGained: 2 }); expect(useCombatStore.getState()).toMatchObject({ level: 3, xp: 300, xpToNext: 1_400 }); const second = useCombatStore.getState().damageMob("boss", 10, 2000); expect(second).toMatchObject({ amount: 0, killed: false, xpAwarded: 0 }); expect(useCombatStore.getState().level).toBe(3); }); it("damages every live mob inside a self-centered area ability", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 2 }); for (const [id, position] of [ ["near-a", [3, 0, 0]], ["near-b", [8, 0, 0]], ["far", [18, 0, 0]], ] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 50 }); setMobPosition(id, position); } const result = useCombatStore.getState().castSlot(2, [0, 0, 0], 1000); expect(result.affectedMobIds).toEqual(["near-a", "near-b"]); expect(useCombatStore.getState().mobs["near-a"].health).toBeLessThan(50); expect(useCombatStore.getState().mobs["near-b"].health).toBe( useCombatStore.getState().mobs["near-a"].health, ); expect(useCombatStore.getState().mobs.far.health).toBe(50); }); it("engages an attacked pack and gives every mob an independent melee swing", () => { for (const [id, position] of [["pack:a", [2, 0, 0]], ["pack:b", [3, 0, 0]]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100 }); setMobPosition(id, position); } useCombatStore.getState().selectMob("pack:a"); const healthBefore = useCombatStore.getState().health; expect(useCombatStore.getState().advanceMobCombat(1000, [0, 0, 0])).toBe(0); expect(useCombatStore.getState().health).toBe(healthBefore); useCombatStore.getState().damageMob("pack:a", 1, 1000); expect(useCombatStore.getState().mobs["pack:a"].engaged).toBe(true); expect(useCombatStore.getState().mobs["pack:b"].engaged).toBe(true); expect(useCombatStore.getState().mobs["pack:a"].woundRevision).toBe(1); expect(useCombatStore.getState().mobs["pack:a"].attackRevision).toBe(0); expect(useCombatStore.getState().advanceMobCombat(1449, [0, 0, 0])).toBe(0); expect(useCombatStore.getState().mobs["pack:a"].attackRevision).toBe(0); expect(useCombatStore.getState().advanceMobCombat(1450, [0, 0, 0])).toBe(12); expect(useCombatStore.getState().mobs["pack:a"].attackRevision).toBe(1); expect(useCombatStore.getState().mobs["pack:b"].attackRevision).toBe(1); expect(useCombatStore.getState().health).toBe(healthBefore - 12); }); it("automatically engages a dungeon pack when a living party actor enters its aggro range", () => { const partyMemberId = preparePartyMember(0, [40, 0, 0]); for (const [id, position] of [["proximity-pack:a", [20, 0, 0]], ["proximity-pack:b", [22, 0, 0]]] as const) { setMobPosition(id, position); useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100, }); } setMobPosition("proximity-boss", [100, 0, 0]); useCombatStore.getState().registerMob("proximity-boss", { name: "Proximity Boss", boss: true, maxHealth: 100, }); expect(useCombatStore.getState().mobs["proximity-pack:a"].aggroRange).toBe(16); expect(useCombatStore.getState().mobs["proximity-boss"].aggroRange).toBe(20); expect(useCombatStore.getState().engageNearbyMobs(1_000, [0, 0, 0])).toBe(0); expect(useCombatStore.getState().mobs["proximity-pack:a"].engaged).toBe(false); updatePartyRuntimePosition(partyMemberId, 27, 0, 0); expect(useCombatStore.getState().engageNearbyMobs(1_100, [0, 0, 0])).toBe(1); expect(useCombatStore.getState().mobs["proximity-pack:a"]).toMatchObject({ engaged: true, targetActorId: partyMemberId, homePosition: [20, 0, 0], }); expect(useCombatStore.getState().mobs["proximity-pack:b"]).toMatchObject({ engaged: true, targetActorId: partyMemberId, homePosition: [22, 0, 0], }); }); it("uses 3D distance for proximity aggro so stacked dungeon floors do not pull", () => { setMobPosition("upper-floor-mob", [0, 20, 0]); useCombatStore.getState().registerMob("upper-floor-mob", { name: "Upper Floor Mob", maxHealth: 100, }); expect(useCombatStore.getState().engageNearbyMobs(1_000, [0, 0, 0])).toBe(0); expect(useCombatStore.getState().mobs["upper-floor-mob"].engaged).toBe(false); expect(useCombatStore.getState().engageNearbyMobs(1_100, [0, 8, 0])).toBe(1); expect(useCombatStore.getState().mobs["upper-floor-mob"].engaged).toBe(true); }); it("anchors a roaming mob at the pull location and keeps aggro across vertical terrain", () => { const id = "kresh-patrol:kresh-roaming"; setMobPosition(id, [0, -70, 0]); useCombatStore.getState().registerMob(id, { name: "Kresh", boss: true, maxHealth: 100, leashRange: 55, xpReward: 0, }); // Kresh can travel far from the position where his model first mounted. // His leash must begin at the patrol position where combat actually starts. setMobPosition(id, [100, -106, 0]); useCombatStore.getState().damageMob(id, 10, 1_000); expect(useCombatStore.getState().mobs[id]).toMatchObject({ health: 90, engaged: true, homePosition: [100, -106, 0], }); // A player falling from the ledge can be vertically farther than the leash // while remaining beside the encounter in the dungeon's horizontal plane. useCombatStore.getState().advanceMobCombat(1_450, [102, -30, 0]); expect(useCombatStore.getState().mobs[id]).toMatchObject({ health: 90, engaged: true, combatPhase: "chasing", }); // Horizontal distance still enforces the normal evade/reset boundary. useCombatStore.getState().advanceMobCombat(1_500, [156, -106, 0]); expect(useCombatStore.getState().mobs[id]).toMatchObject({ health: 100, engaged: false, combatPhase: "returning", }); }); it("tracks damage and effective-healing threat independently for every engaged mob", () => { for (const id of ["threat-pack:a", "threat-pack:b"]) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 500, xpReward: 0 }); setMobPosition(id, [2, 0, 0]); } useCombatStore.getState().damageMob( "threat-pack:a", 10, 1_000, { actorId: "tank", role: "tank" }, ); useCombatStore.getState().damageMob( "threat-pack:a", 20, 1_100, { actorId: "dps", role: "damage" }, ); useCombatStore.getState().addHealingThreat( { actorId: "healer", role: "healer" }, 80, ); expect(useCombatStore.getState().mobs["threat-pack:a"].threatByActor).toEqual({ tank: 51, dps: 20, healer: 10, }); expect(useCombatStore.getState().mobs["threat-pack:b"].threatByActor).toEqual({ tank: 1, healer: 10, }); }); it("taunts bosses at max plus one, forces immediately, and resumes normal threat after expiry", () => { const tank = usePartyStore.getState().members.find((member) => member.role === "tank")!; registerPartyRuntimePosition(tank.id, [0, 0, 0]); useCombatStore.getState().registerMob("taunt-boss", { name: "Taunt boss", boss: true, maxHealth: 500, xpReward: 0, }); setMobPosition("taunt-boss", [2, 0, 0]); useCombatStore.getState().damageMob("taunt-boss", 40, 1_000, { actorId: "__player__", role: "damage", }); expect(useCombatStore.getState().tauntMob( "taunt-boss", { actorId: tank.id, role: "tank" }, 3_000, 2_000, )).toBe(true); expect(useCombatStore.getState().mobs["taunt-boss"]).toMatchObject({ targetActorId: tank.id, forcedTarget: { actorId: tank.id, endsAt: 5_000 }, threatByActor: { "__player__": 41, [tank.id]: 42, }, }); useCombatStore.getState().damageMob("taunt-boss", 100, 3_000, { actorId: "__player__", role: "damage", }); useCombatStore.getState().advanceMobCombat(4_999); expect(useCombatStore.getState().mobs["taunt-boss"].targetActorId).toBe(tank.id); useCombatStore.getState().advanceMobCombat(5_000); expect(useCombatStore.getState().mobs["taunt-boss"]).toMatchObject({ targetActorId: "__player__", forcedTarget: null, }); }); it("swings at the resolved companion without redirecting player damage", () => { const target = usePartyStore.getState().members.find((member) => member.role === "damage")!; usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === target.id ? { ...member, gearStats: { ...EMPTY_ITEM_STATS, armor: 1_000 } } : member), })); registerPartyRuntimePosition(target.id, [0, 0, 0]); useCombatStore.getState().registerMob("party-victim", { name: "Party victim", maxHealth: 200, xpReward: 0, }); setMobPosition("party-victim", [2, 0, 0]); useCombatStore.getState().damageMob("party-victim", 5, 1_000, { actorId: target.id, role: "damage", }); const playerBefore = useCombatStore.getState().health; const memberBefore = target.health; const resolvedDamage = useCombatStore.getState().advanceMobCombat(1_450); expect(resolvedDamage).toBeGreaterThan(0); expect(useCombatStore.getState().health).toBe(playerBefore); expect(usePartyStore.getState().members.find((member) => member.id === target.id)?.health) .toBe(memberBefore - resolvedDamage); expect(useCombatStore.getState().lastCombatResult?.finalDamage).toBe(resolvedDamage); expect(useCombatStore.getState().mobs["party-victim"].targetActorId).toBe(target.id); }); it("clears all aggro state on encounter reset", () => { registerTarget("reset-threat", [2, 0, 0], 100, 0); useCombatStore.getState().damageMob("reset-threat", 1, 1_000); useCombatStore.getState().tauntMob( "reset-threat", { actorId: "__player__", role: "healer" }, 3_000, 1_100, ); useCombatStore.getState().resetEncounter(); expect(useCombatStore.getState().mobs["reset-threat"]).toMatchObject({ threatByActor: {}, targetActorId: null, forcedTarget: null, engaged: false, }); }); it("throttles wound animation revisions during simultaneous party damage", () => { useCombatStore.getState().registerMob("focus-target", { name: "focus-target", maxHealth: 100 }); useCombatStore.getState().damageMob("focus-target", 1, 1_000); useCombatStore.getState().damageMob("focus-target", 1, 1_000); useCombatStore.getState().damageMob("focus-target", 1, 1_100); expect(useCombatStore.getState().mobs["focus-target"].woundRevision).toBe(1); useCombatStore.getState().damageMob( "focus-target", 1, 1_000 + MOB_WOUND_ANIMATION_COOLDOWN_MS, ); expect(useCombatStore.getState().mobs["focus-target"].woundRevision).toBe(2); }); it("holds lootable corpses until looted and expires empty corpses after ten seconds", () => { useCombatStore.getState().registerMob("empty", { name: "empty", maxHealth: 1 }); useCombatStore.getState().damageMob("empty", 1, 1_000); expect(useCombatStore.getState().mobs.empty).toMatchObject({ dead: true, despawned: false, corpseDespawnAt: 1_000 + EMPTY_CORPSE_DESPAWN_MS, }); useCombatStore.getState().tick(0, 1_000 + EMPTY_CORPSE_DESPAWN_MS - 1); expect(useCombatStore.getState().mobs.empty.despawned).toBe(false); useCombatStore.getState().tick(0, 1_000 + EMPTY_CORPSE_DESPAWN_MS); expect(useCombatStore.getState().mobs.empty.despawned).toBe(true); useCombatStore.getState().registerMob("lootable", { name: "lootable", maxHealth: 1, hasLoot: true, }); useCombatStore.getState().damageMob("lootable", 1, 20_000); useCombatStore.getState().tick(0, 60_000); expect(useCombatStore.getState().mobs.lootable).toMatchObject({ dead: true, looted: false, despawned: false, corpseDespawnAt: null, }); expect(useCombatStore.getState().lootMob("lootable", 20_100)).toBe(true); expect(useCombatStore.getState().mobs.lootable).toMatchObject({ looted: true, despawned: false, corpseDespawnAt: 20_000 + MINIMUM_CORPSE_VISIBLE_MS, }); useCombatStore.getState().tick(0, 20_000 + MINIMUM_CORPSE_VISIBLE_MS); expect(useCombatStore.getState().mobs.lootable.despawned).toBe(true); expect(useCombatStore.getState().lootMob("lootable", 30_000)).toBe(false); }); it("rolls SQL boss loot at the killing player's level and grants it automatically exactly once", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 1, xp: 360, inventory: [], }); useCombatStore.getState().registerMob("anacondra-spawn", { name: "Lady Anacondra", boss: true, maxHealth: 1, xpReward: 20, lootSourceId: "lady-anacondra", }); expect(useCombatStore.getState().damageMob("anacondra-spawn", 1, 5_000)).toMatchObject({ killed: true, levelsGained: 1, }); const corpse = useCombatStore.getState().mobs["anacondra-spawn"]; expect(useCombatStore.getState().level).toBe(2); expect(corpse).toMatchObject({ dead: true, hasLoot: true, looted: true, corpseDespawnAt: 5_000 + MINIMUM_CORPSE_VISIBLE_MS, }); expect(corpse.pendingLoot).toHaveLength(1); expect(corpse.pendingLoot[0]).toMatchObject({ itemLevel: 1, sourceBossKey: "lady-anacondra", }); expect(useCombatStore.getState().inventory).toEqual(corpse.pendingLoot); expect(useCombatStore.getState().bossLootRewards).toEqual([ expect.objectContaining({ bossId: "anacondra-spawn", bossName: "Lady Anacondra", items: corpse.pendingLoot, acquiredAt: 5_000, }), ]); useCombatStore.getState().resetEncounter(); expect(useCombatStore.getState().bossLootRewards).toHaveLength(1); useCombatStore.getState().dismissBossLootReward(); expect(useCombatStore.getState().bossLootRewards).toEqual([]); expect(useCombatStore.getState().lootMob("anacondra-spawn", 5_100)).toBe(false); expect(useCombatStore.getState().lootMob("anacondra-spawn", 5_200)).toBe(false); expect(useCombatStore.getState().inventory).toHaveLength(1); }); it("aggros every surviving packmate even when the pulled mob dies to the opening hit", () => { for (const [id, maxHealth] of [["one-shot:a", 1], ["one-shot:b", 100], ["outsider", 100]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth, xpReward: 0 }); setMobPosition(id, [2, 0, 0]); } expect(useCombatStore.getState().damageMob("one-shot:a", 99, 1000)).toMatchObject({ killed: true }); expect(useCombatStore.getState().mobs["one-shot:a"]).toMatchObject({ dead: true, engaged: false }); expect(useCombatStore.getState().mobs["one-shot:b"]).toMatchObject({ dead: false, engaged: true }); expect(useCombatStore.getState().mobs.outsider.engaged).toBe(false); }); it("fears every living non-boss enemy within Psychic Scream's eight-yard radius for eight seconds", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 14 }); const nearby = ["scream-pack:a", "near-b", "near-c", "near-d"] as const; nearby.forEach((id, index) => { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100 }); setMobPosition(id, [index + 1, 0, 0]); }); useCombatStore.getState().registerMob("scream-pack:b", { name: "outside packmate", maxHealth: 100 }); setMobPosition("scream-pack:b", [12, 0, 0]); useCombatStore.getState().registerMob("near-boss", { name: "immune boss", boss: true, maxHealth: 100, mechanicImmunities: ["fear"], }); setMobPosition("near-boss", [7, 0, 0]); useCombatStore.getState().registerMob("fallen", { name: "fallen", maxHealth: 1, xpReward: 0 }); setMobPosition("fallen", [5, 0, 0]); useCombatStore.getState().damageMob("fallen", 1, 500); const result = useCombatStore.getState().castAbility("priest-psychic-scream", [0, 0, 0], 1000); expect(result.ok).toBe(true); expect(result.affectedMobIds).toHaveLength(5); for (const id of nearby) { expect(useCombatStore.getState().mobs[id].statuses).toContainEqual({ kind: "fear", sourceAbilityId: "priest-psychic-scream", endsAt: 9000, }); } expect(useCombatStore.getState().mobs["scream-pack:b"]).toMatchObject({ engaged: true, statuses: [] }); expect(useCombatStore.getState().mobs["near-boss"].statuses).toEqual([]); expect(useCombatStore.getState().mobs.fallen.statuses).toEqual([]); expect(useCombatStore.getState().advanceMobCombat(8999, [0, 0, 0])).toBe(0); useCombatStore.getState().tick(0.001, 9000); expect(useCombatStore.getState().mobs["scream-pack:a"].statuses).toEqual([]); expect(useCombatStore.getState().advanceMobCombat(9000, [0, 0, 0])).toBeGreaterThan(0); }); it("interrupts a hard cast when the player moves", () => { useCombatStore.getState().initializeCharacter({ classId: "priest" }); registerTarget("fang", [10, 0, 0]); expect(useCombatStore.getState().castSlot(0, [0, 0, 0], 1000).ok).toBe(true); expect(useCombatStore.getState().playerAnimationEvent).toMatchObject({ kind: "ability-start", abilityId: "priest-smite", }); useCombatStore.getState().setPlayerPosition([0.5, 0, 0]); expect(useCombatStore.getState().activeCast).toBeNull(); expect(useCombatStore.getState().playerAnimationEvent).toMatchObject({ kind: "ability-cancel", abilityId: "priest-smite", }); useCombatStore.getState().tick(0.1, 3000); expect(useCombatStore.getState().mobs.fang.health).toBe(100); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); }); it("cycles nearest living targets in both directions", () => { for (const [id, distance] of [["one", 2], ["two", 4], ["three", 6]] as const) { useCombatStore.getState().registerMob(id, { name: id }); setMobPosition(id, [distance, 0, 0]); } expect(useCombatStore.getState().cycleTarget("next")).toBe("one"); expect(useCombatStore.getState().cycleTarget("next")).toBe("two"); expect(useCombatStore.getState().cycleTarget("previous")).toBe("one"); expect(selectedMob(useCombatStore.getState())?.id).toBe("one"); }); it("tracks rogue combo points and consumes them with Eviscerate", () => { useCombatStore.getState().initializeCharacter({ classId: "rogue" }); registerTarget("fang", [2, 0, 0], 100); expect(useCombatStore.getState().castSlot(0, undefined, 1000).ok).toBe(true); expect(useCombatStore.getState().comboPoints).toBe(1); expect(useCombatStore.getState().castSlot(1, undefined, 2000).ok).toBe(true); expect(useCombatStore.getState().comboPoints).toBe(0); expect(useCombatStore.getState().mobs.fang.health).toBeLessThan(100); expect(useCombatStore.getState().lastCombatResult).not.toBeNull(); }); it("shares the shaman Shock cooldown family", () => { useCombatStore.getState().initializeCharacter({ classId: "shaman", level: 4 }); registerTarget("druid", [10, 0, 0]); expect(useCombatStore.getState().castSlot(1, undefined, 1000).ok).toBe(true); expect(useCombatStore.getState().cooldowns["shaman-flame-shock"]).toBe(7000); expect(useCombatStore.getState().cooldowns["shaman-earth-shock"]).toBe(7000); }); it("allocates the first WotLK talent point at level 10 and clamps settings", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 10 }); const holy = talentNodesForTree("priest-holy")[0]; const shadow = talentNodesForTree("priest-shadow")[0]; expect(useCombatStore.getState().allocateTalent(holy.id)).toBe(true); expect(useCombatStore.getState().allocateTalent(shadow.id)).toBe(false); expect(useCombatStore.getState().talentRanks).toEqual({ [holy.id]: 1 }); useCombatStore.getState().resetTalents(); expect(useCombatStore.getState().talentRanks).toEqual({}); expect(useCombatStore.getState().settings.showThreatMeter).toBe(false); expect(useCombatStore.getState().settings.showMobAggroRanges).toBe(false); useCombatStore.getState().updateSettings({ masterVolume: 9, uiScale: 0.1, minimapRotation: "north-up", showThreatMeter: true, showMobAggroRanges: true, threatMeterPosition: { x: -2, y: 4 }, }); expect(useCombatStore.getState().settings).toMatchObject({ masterVolume: 1, uiScale: 0.75, minimapRotation: "north-up", showThreatMeter: true, showMobAggroRanges: true, threatMeterPosition: { x: 0, y: 1 }, }); useCombatStore.getState().initializeCharacter({ classId: "priest" }); expect(useCombatStore.getState().settings.showThreatMeter).toBe(false); useCombatStore.getState().resetSettings(); expect(useCombatStore.getState().settings).toEqual(DEFAULT_GAMEPLAY_SETTINGS); }); it("keeps a CoA tree ability uncastable until its exact node is purchased", () => { useCombatStore.getState().initializeCharacter({ classId: "stormbringer", level: 60 }); const ability = abilitiesForClass("stormbringer").find((candidate) => ( candidate.source === "coa-tree" && !candidate.passive && candidate.talentEntryId && TALENT_NODES.some((node) => ( node.id === `coa-entry-${candidate.talentEntryId}` && node.prerequisites.length === 0 && (node.requiredClassPoints ?? 0) === 0 && (node.requiredSpecPoints ?? 0) === 0 )) ))!; expect(ability).toBeDefined(); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000)).toMatchObject({ ok: false, reason: "not-learned", }); expect(useCombatStore.getState().allocateTalent(`coa-entry-${ability.talentEntryId}`)).toBe(true); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 3_000).reason).not.toBe("not-learned"); }); it("resets encounter health without erasing earned progression", () => { registerTarget("mob", [2, 0, 0], 1, 100); useCombatStore.getState().damageMob("mob", 5, 1000); expect(useCombatStore.getState().level).toBe(2); useCombatStore.getState().resetEncounter(); expect(useCombatStore.getState()).toMatchObject({ level: 2, selectedTargetId: null }); expect(useCombatStore.getState().mobs.mob).toMatchObject({ dead: false, health: 1, experienceGranted: false }); }); it("applies learned flat cast-time and cooldown claims to their named mage spells", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 }); allocateTalentRanks("Improved Frostbolt", 1); allocateTalentRanks("Improved Fire Blast", 1); registerTarget("talent-dummy", [10, 0, 0], 10_000, 0); expect(useCombatStore.getState().castSlot(0, undefined, 1_000).ok).toBe(true); expect(useCombatStore.getState().activeCast?.completesAt).toBe(4_000); useCombatStore.getState().tick(0.1, 4_000); expect(useCombatStore.getState().castSlot(1, undefined, 4_100).ok).toBe(true); expect(useCombatStore.getState().cooldowns["mage-fire-blast"]).toBeGreaterThan(4_100); }); it("uses WoW 3.3.5a Shadow Word: Pain periodic damage", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 }); allocateTalentRanks("Twin Disciplines", 5); registerTarget("pain-dummy", [10, 0, 0], 1_000, 0); expect(useCombatStore.getState().castSlot(1, undefined, 1_000).ok).toBe(true); useCombatStore.getState().tick(0.1, 4_000); // Twin Disciplines is Holy-only in the current talent data, so it does // not modify this Shadow spell. expect(useCombatStore.getState().mobs["pain-dummy"].health).toBe(770); }); it("applies learned healing and resource-cost percentages to exact outcomes", () => { useCombatStore.getState().initializeCharacter({ classId: "paladin", level: 80 }); allocateTalentRanks("Divinity", 3); useCombatStore.getState().damagePlayer(1_000); const beforeHeal = useCombatStore.getState().health; expect(useCombatStore.getState().castSlot(1, undefined, 1_000).ok).toBe(true); const healCompletion = useCombatStore.getState().activeCast!.completesAt; useCombatStore.getState().tick(0.1, healCompletion); expect(useCombatStore.getState().health - beforeHeal).toBeGreaterThan(719); useCombatStore.getState().initializeCharacter({ classId: "shaman", level: 80 }); allocateTalentRanks("Convection", 5); const startingMana = useCombatStore.getState().resource; expect(useCombatStore.getState().talentModifiers).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "resource-cost-percent", amount: -15, sourceTalentName: "Convection" }), ])); registerTarget("mana-dummy", [10, 0, 0], 10_000, 0); expect(useCombatStore.getState().castSlot(0, undefined, 10_000).ok).toBe(true); const boltCompletion = useCombatStore.getState().activeCast!.completesAt; useCombatStore.getState().tick(0.1, boltCompletion); expect(useCombatStore.getState().resource).toBe(startingMana - 9); }); it("routes a cast-time direct heal to the selected living party member", () => { registerTarget("hostile-selection", [10, 0, 0], 100, 0); const memberId = preparePartyMember(0, [10, 0, 0], 1, true); useCombatStore.getState().damagePlayer(25); const playerHealthBefore = useCombatStore.getState().health; expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 1_000)).toMatchObject({ ok: true, healing: 0, }); expect(useCombatStore.getState().activeCast?.targetId).toBe(memberId); expect(useCombatStore.getState().selectedTargetId).toBe("hostile-selection"); useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt); expect(usePartyStore.getState().members.find((member) => member.id === memberId)!.health).toBeGreaterThan(1); expect(useCombatStore.getState().health).toBe(playerHealthBefore); expect(useCombatStore.getState().selectedTargetId).toBe("hostile-selection"); }); it("falls back to the player and snapshots an explicit null friendly target", () => { registerTarget("enemy", [10, 0, 0], 100, 0); const memberId = preparePartyMember(0, [8, 0, 0], 1); useCombatStore.getState().damagePlayer(40); const playerHealthBefore = useCombatStore.getState().health; expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 1_000).ok).toBe(true); expect(useCombatStore.getState().activeCast?.targetId).toBeNull(); usePartyStore.getState().selectMember(memberId); useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt); expect(useCombatStore.getState().health).toBeGreaterThan(playerHealthBefore); expect(usePartyStore.getState().members.find((member) => member.id === memberId)!.health).toBe(1); expect(useCombatStore.getState().selectedTargetId).toBe("enemy"); }); it("routes an explicit self selection to the player", () => { const memberId = preparePartyMember(0, [8, 0, 0], 1, true); useCombatStore.getState().damagePlayer(40); const playerHealthBefore = useCombatStore.getState().health; usePartyStore.getState().selectSelf(); expect(usePartyStore.getState()).toMatchObject({ selectedMemberId: null, selfSelected: true, }); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 1_000).ok).toBe(true); expect(useCombatStore.getState().activeCast?.targetId).toBeNull(); useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt); expect(useCombatStore.getState().health).toBeGreaterThan(playerHealthBefore); expect(usePartyStore.getState().members.find((member) => member.id === memberId)!.health).toBe(1); }); it("rejects unavailable, out-of-range, and down friendly targets before spending resources", () => { const unavailableId = preparePartyMember(0, [10, 0, 0], 1, true); clearPartyRuntimeRegistry(); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 1_000)).toMatchObject({ ok: false, reason: "target-unavailable", }); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); expect(usePartyStore.getState().selectedMemberId).toBe(unavailableId); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 1 }); usePartyStore.getState().initializeParty("range-test", 1, useCombatStore.getState().health); clearPartyRuntimeRegistry(); preparePartyMember(0, [40.01, 0, 0], 1, true); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 2_000)).toMatchObject({ ok: false, reason: "out-of-range", }); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 1 }); usePartyStore.getState().initializeParty("down-test", 1, useCombatStore.getState().health); clearPartyRuntimeRegistry(); const downId = preparePartyMember(0, [10, 0, 0], 1, true); setPartyMemberHealth(downId, 0); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 3_000)).toMatchObject({ ok: false, reason: "target-dead", }); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); }); it("revalidates the snapshotted friendly target at cast completion", () => { const memberId = preparePartyMember(0, [40, 0, 0], 1, true); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 1_000).ok).toBe(true); const outOfRangeCompletion = useCombatStore.getState().activeCast!.completesAt; updatePartyRuntimePosition(memberId, 40.01, 0, 0); useCombatStore.getState().tick(0.1, outOfRangeCompletion); expect(usePartyStore.getState().members.find((member) => member.id === memberId)!.health).toBe(1); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); expect(useCombatStore.getState().feedback?.message).toBe("That target is out of range."); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 1 }); usePartyStore.getState().initializeParty("completion-death", 1, useCombatStore.getState().health); clearPartyRuntimeRegistry(); const downId = preparePartyMember(0, [10, 0, 0], 1, true); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 3_000).ok).toBe(true); const defeatedCompletion = useCombatStore.getState().activeCast!.completesAt; setPartyMemberHealth(downId, 0); useCombatStore.getState().tick(0.1, defeatedCompletion); expect(usePartyStore.getState().members.find((member) => member.id === downId)!.health).toBe(0); expect(useCombatStore.getState().resource).toBe(useCombatStore.getState().maxResource); expect(useCombatStore.getState().feedback?.message).toBe("That target is already defeated."); }); it("keeps a cast-time friendly heal on its original ally after selection changes", () => { const firstId = preparePartyMember(0, [8, 0, 0], 1, true); const secondId = preparePartyMember(1, [9, 0, 0], 1); expect(useCombatStore.getState().castAbility("wow335-priest-lesser-heal", [0, 0, 0], 1_000).ok).toBe(true); usePartyStore.getState().selectMember(secondId); useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt); expect(usePartyStore.getState().members.find((member) => member.id === firstId)!.health).toBeGreaterThan(1); expect(usePartyStore.getState().members.find((member) => member.id === secondId)!.health).toBe(1); }); it("tracks independent Rejuvenation HoTs per ally and refreshes only the same target", () => { useCombatStore.getState().initializeCharacter({ classId: "druid", level: 1 }); const firstId = preparePartyMember(0, [8, 0, 0], 1, true); const secondId = preparePartyMember(1, [9, 0, 0], 1); useCombatStore.getState().damagePlayer(30); const playerHealthBefore = useCombatStore.getState().health; expect(useCombatStore.getState().castAbility("druid-rejuvenation", [0, 0, 0], 1_000).ok).toBe(true); usePartyStore.getState().selectMember(secondId); expect(useCombatStore.getState().castAbility("druid-rejuvenation", [0, 0, 0], 2_500).ok).toBe(true); expect(useCombatStore.getState().effects.map((effect) => effect.targetId)).toEqual( expect.arrayContaining([firstId, secondId]), ); expect(useCombatStore.getState().effects).toHaveLength(2); useCombatStore.getState().tick(0.1, 4_000); expect(usePartyStore.getState().members.find((member) => member.id === firstId)!.health).toBeGreaterThan(1); expect(usePartyStore.getState().members.find((member) => member.id === secondId)!.health).toBe(1); useCombatStore.getState().tick(0.1, 5_500); expect(usePartyStore.getState().members.find((member) => member.id === secondId)!.health).toBeGreaterThan(1); expect(useCombatStore.getState().health).toBe(playerHealthBefore); usePartyStore.getState().selectMember(firstId); expect(useCombatStore.getState().castAbility("druid-rejuvenation", [0, 0, 0], 5_500).ok).toBe(true); expect(useCombatStore.getState().effects.filter((effect) => effect.targetId === firstId)).toHaveLength(1); expect(useCombatStore.getState().effects.filter((effect) => effect.targetId === secondId)).toHaveLength(1); }); it("keeps healing components of hostile abilities on the player", () => { useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 2 }); useCombatStore.setState({ resource: 100 }); registerTarget("death-strike-target", [2, 0, 0], 100, 0); const memberId = preparePartyMember(0, [2, 0, 0], 1, true); useCombatStore.getState().damagePlayer(40); const playerHealthBefore = useCombatStore.getState().health; const result = useCombatStore.getState().castAbility("death-knight-death-strike", [0, 0, 0], 1_000); expect(result.ok).toBe(true); expect(result.damage).toBeGreaterThan(0); expect(result.healing).toBeGreaterThan(0); expect(useCombatStore.getState().health).toBeGreaterThan(playerHealthBefore); expect(usePartyStore.getState().members.find((member) => member.id === memberId)!.health).toBe(1); expect(useCombatStore.getState().selectedTargetId).toBe("death-strike-target"); }); it("does not misapply pet-only Fel Vitality to player health or mana", () => { useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 80 }); allocateTalentRanks("Improved Healthstone", 2); allocateTalentRanks("Improved Imp", 3); const beforeFelVitality = { maxHealth: useCombatStore.getState().maxHealth, maxResource: useCombatStore.getState().maxResource, }; allocateTalentRanks("Fel Vitality", 1); expect(useCombatStore.getState()).toMatchObject(beforeFelVitality); }); it("applies Manastorm boss multipliers and a single bonus-loot chance at registration", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 10 }); useCombatStore.getState().registerMob("storm-boss", { name: "Lady Anacondra", boss: true, lootSourceId: "lady-anacondra", healthMultiplier: 2.02, damageMultiplier: 2.02, bonusLootChance: 1, }); expect(useCombatStore.getState().mobs["storm-boss"]).toMatchObject({ maxHealth: 1_838, attackDamage: 65, healthMultiplier: 2.02, damageMultiplier: 2.02, bonusLootChance: 1, }); useCombatStore.getState().damageMob("storm-boss", 10_000, 30_000); expect(useCombatStore.getState().mobs["storm-boss"].pendingLoot).toHaveLength(2); }); });