import { beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore, stableCombatUnit, PLAYER_AURA_ENTITY_ID } from "./combatStore"; import { DEFAULT_ENEMY_ATTACK, type EnemyDamageSchool } from "./enemyAttacks"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { WOW335_DEATH_KNIGHT_SPELL_DEFLECTION_TALENT_ID } from "./wow335DeathKnightSpellDeflectionRuntime"; const ATTACK = { ...DEFAULT_ENEMY_ATTACK, id: "spell-deflection-incoming-test", name: "Spell Deflection incoming test", delivery: "projectile" as const, animation: "cast" as const, castTimeMs: 0, range: 20, cooldownMs: 10_000, initialCooldownMs: [0, 0] as const, repeatCooldownMs: [10_000, 10_000] as const, damageMultiplier: 1, }; const MAX_PARRY_GEAR = { ...EMPTY_ITEM_STATS, parryRating: 50_000 }; function incomingEvent(mobId: string, targetId: string) { const event = useCombatStore.getState().combatEvents.find((candidate) => ( candidate.kind === "damage" && candidate.sourceActorId === mobId && candidate.targetActorId === targetId )); if (!event) throw new Error(`Expected an incoming damage event on ${targetId}.`); return event; } function hitTime(mobId: string, targetId: string): number { for (let now = 1_000; now < 200_000; now += 1) { const resultRoll = stableCombatUnit(`${mobId}:${ATTACK.id}:${targetId}:0:${now}:outcome`); if (resultRoll >= 0.2 && resultRoll < 0.9) return now; } throw new Error("Could not find a landed incoming spell hit seed."); } function setupAttacker(mobId: string, targetId: string, now: number, school: EnemyDamageSchool): void { useCombatStore.getState().registerMob(mobId, { name: "Spell Deflection test attacker", level: 80, maxHealth: 100_000, xpReward: 0, attacks: [{ ...ATTACK, school }], }); setMobPosition(mobId, [2, 0, 0]); useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [mobId]: { ...state.mobs[mobId]!, attackDamage: 4_000 } }, })); useCombatStore.getState().damageMob(mobId, 1, now - 1, { actorId: targetId, role: "tank" }, "physical"); } function resetForHit(playerClass: "death-knight" | "warrior", runId: string): void { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useCombatStore.getState().initializeCharacter({ classId: playerClass, level: 80, xp: 0 }); usePartyStore.getState().initializeParty( `wow335-spell-deflection-${runId}`, 80, useCombatStore.getState().health, "tank", 1, ); } function runPlayerHit(rank: number) { resetForHit("death-knight", `player-${rank}`); useCombatStore.setState((state) => ({ gearStats: MAX_PARRY_GEAR, talentRanks: { ...state.talentRanks, [WOW335_DEATH_KNIGHT_SPELL_DEFLECTION_TALENT_ID]: rank }, })); const mobId = "spell-deflection-player-attacker"; const now = hitTime(mobId, PLAYER_AGGRO_ID); setupAttacker(mobId, PLAYER_AGGRO_ID, now, "shadow"); useCombatStore.getState().advanceMobCombat(now); return incomingEvent(mobId, PLAYER_AURA_ENTITY_ID); } function runCompanionHit(rank: number) { resetForHit("warrior", `companion-${rank}`); const talentRanks = { [WOW335_DEATH_KNIGHT_SPELL_DEFLECTION_TALENT_ID]: rank }; const member = createConfiguredPartyMember({ name: "Spell Deflection DK", classId: "death-knight", specialization: "Blood", specializationId: "death-knight:blood", talentTreeId: "death-knight-blood", role: "tank", talentRanks, }, 80, 0, `wow335-spell-deflection-companion-${rank}`); usePartyStore.setState({ members: [{ ...member, health: 100_000, maxHealth: 100_000, gearStats: MAX_PARRY_GEAR, talentRanks, globalCooldownEndsAt: 61_000, nextActionAt: 61_000, }], }); registerPartyRuntimePosition(member.id, [0, 0, 0]); useCombatStore.getState().synchronizeActors(); const mobId = "spell-deflection-companion-attacker"; const now = hitTime(mobId, member.id); setupAttacker(mobId, member.id, now, "shadow"); advanceDungeonPartyCombat(now); useCombatStore.getState().advanceMobCombat(now, [100, 0, 0]); return incomingEvent(mobId, member.id); } describe("WoW Death Knight Spell Deflection combat integration", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); }); it("absorbs the rank percentage of a direct shadow hit for player and generated companion", () => { const playerBaseline = runPlayerHit(0); const playerDeflected = runPlayerHit(3); const companionBaseline = runCompanionHit(0); const companionDeflected = runCompanionHit(3); expect(playerBaseline.effectiveAmount).toBeGreaterThan(0); expect(playerBaseline.absorbed).toBe(0); expect(playerDeflected.absorbed).toBe(Math.floor(playerBaseline.effectiveAmount * 0.45)); expect(playerDeflected.effectiveAmount).toBe(playerBaseline.effectiveAmount - playerDeflected.absorbed); expect(companionBaseline.effectiveAmount).toBeGreaterThan(0); expect(companionBaseline.absorbed).toBe(0); expect(companionDeflected.absorbed).toBe(Math.floor(companionBaseline.effectiveAmount * 0.45)); expect(companionDeflected.effectiveAmount).toBe(companionBaseline.effectiveAmount - companionDeflected.absorbed); }); });