import { beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore, stableCombatUnit } from "./combatStore"; import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { WOW335_PALADIN_TOUCHED_BY_THE_LIGHT_TALENT_ID, WOW335_WARRIOR_CRITICAL_BLOCK_TALENT_ID, } from "./wow335WarriorCriticalBlockRuntime"; const SWING = { ...DEFAULT_ENEMY_ATTACK, id: "critical-block-test-swing", name: "Critical Block Test Swing", cooldownMs: 10_000, initialCooldownMs: [0, 0] as const, repeatCooldownMs: [10_000, 10_000] as const, damageMultiplier: 1, }; function timeForBlock(mobId: string, actorId: string): number { for (let now = 1_000; now < 200_000; now += 1) { const roll = stableCombatUnit(`${mobId}:${SWING.id}:${actorId}:0:${now}:outcome`); if (roll >= 0.2 && roll < 0.9) return now; } throw new Error("Could not find a stable Critical Block test roll."); } function registerAttacker(mobId: string, targetActorId: string, now: number): void { useCombatStore.getState().registerMob(mobId, { name: mobId, level: 80, maxHealth: 100_000, xpReward: 0, attacks: [SWING], }); setMobPosition(mobId, [2, 0, 0]); useCombatStore.getState().damageMob(mobId, 1, now - 1, { actorId: targetActorId, role: "tank", }, "physical"); } function incomingEvent(mobId: string, targetActorId: string) { const event = useCombatStore.getState().combatEvents.find((candidate) => ( candidate.kind === "damage" && candidate.sourceActorId === mobId && candidate.targetActorId === targetActorId )); if (!event) throw new Error(`Expected a recorded incoming hit on ${targetActorId}.`); return event; } function runPlayerBlock( rank: number, options: { readonly attackerIsPlayer?: boolean; readonly touchedByTheLightRank?: number } = {}, ) { clearMobRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80, role: "tank" }); useCombatStore.setState((state) => ({ health: 100_000, maxHealth: 100_000, gearStats: { ...EMPTY_ITEM_STATS, blockRating: 1_000_000, blockValue: 100 }, talentRanks: { ...state.talentRanks, [WOW335_WARRIOR_CRITICAL_BLOCK_TALENT_ID]: rank, [WOW335_PALADIN_TOUCHED_BY_THE_LIGHT_TALENT_ID]: options.touchedByTheLightRank ?? 0, }, })); const mobId = "critical-block-player-attacker"; const now = timeForBlock(mobId, PLAYER_AGGRO_ID); registerAttacker(mobId, PLAYER_AGGRO_ID, now); if (options.attackerIsPlayer) { useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [mobId]: { ...state.mobs[mobId]!, characterDefender: { level: 80 } }, }, })); } useCombatStore.getState().advanceMobCombat(now); return incomingEvent(mobId, PLAYER_AGGRO_ID); } function runPartyBlock(rank: number) { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 }); const partyId = "critical-block-party"; usePartyStore.getState().initializeParty(partyId, 80, useCombatStore.getState().health, "damage", 1); const warrior = createConfiguredPartyMember({ name: "Critical Block Warrior", classId: "warrior", specialization: "Protection", specializationId: "warrior:protection", talentTreeId: "warrior-protection", talentRanks: { [WOW335_WARRIOR_CRITICAL_BLOCK_TALENT_ID]: rank }, role: "tank", }, 80, 0, partyId); usePartyStore.setState({ members: [{ ...warrior, gearStats: { ...EMPTY_ITEM_STATS, blockRating: 1_000_000, blockValue: 100 }, }], }); registerPartyRuntimePosition(warrior.id, [0, 0, 0]); useCombatStore.getState().synchronizeActors(); const mobId = "critical-block-party-attacker"; const now = timeForBlock(mobId, warrior.id); registerAttacker(mobId, warrior.id, now); advanceDungeonPartyCombat(now); useCombatStore.getState().advanceMobCombat(now); return incomingEvent(mobId, warrior.id); } describe("WoW Warrior Critical Block runtime integration", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 }); usePartyStore.getState().initializeParty("critical-block-tests", 80, useCombatStore.getState().health, "damage", 1); }); it("reduces successful player and generated companion blocks by the generated rank-3 25%", () => { const playerRank0 = runPlayerBlock(0); const playerRank3 = runPlayerBlock(3); expect(playerRank0.outcome).toBe("block"); expect(playerRank3.outcome).toBe("block"); expect(playerRank3.effectiveAmount).toBe(Math.round(playerRank0.effectiveAmount * 0.75)); const partyRank0 = runPartyBlock(0); const partyRank3 = runPartyBlock(3); expect(partyRank0.outcome).toBe("block"); expect(partyRank3.outcome).toBe("block"); expect(partyRank3.effectiveAmount).toBe(Math.round(partyRank0.effectiveAmount * 0.75)); }); it("keeps the generated rank-3 block reduction disabled against player attackers and with Touched by the Light", () => { const ordinary = runPlayerBlock(0); expect(runPlayerBlock(3, { attackerIsPlayer: true })).toMatchObject({ outcome: "block", effectiveAmount: ordinary.effectiveAmount, }); expect(runPlayerBlock(3, { touchedByTheLightRank: 1 })).toMatchObject({ outcome: "block", effectiveAmount: ordinary.effectiveAmount, }); }); });