import { beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; import { useCombatStore } from "./combatStore"; import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks"; import { deriveCharacterStats } from "./wotlkStats"; import { WOW335_ROGUE_UNFAIR_ADVANTAGE_DAMAGE_ID, wow335RogueUnfairAdvantageDamageMultiplierForRank, } from "./wow335RogueTalentRuntime"; function stableUnit(seed: string): number { let hash = 2_166_136_261; for (let index = 0; index < seed.length; index += 1) { hash ^= seed.charCodeAt(index); hash = Math.imul(hash, 16_777_619); } return (hash >>> 0) / 0x1_0000_0000; } function timeForDodge( mobId: string, attackId: string, attackRevision: number, earliest: number, actorId = "__player__", ): number { for (let now = earliest; now < earliest + 30_000; now += 1) { const roll = stableUnit(`${mobId}:${attackId}:${actorId}:${attackRevision}:${now}:outcome`); if (roll >= 0.1 && roll < 0.99) return now; } throw new Error(`Could not find a deterministic dodge roll for ${mobId}:${attackRevision}`); } describe("WoW Rogue Unfair Advantage", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 80 }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); }); const setUpDodgeTarget = (id: string, rank: number, repeatCooldownMs = 3_000) => { const attackId = `${id}-swing`; useCombatStore.setState((state) => ({ gearStats: { ...state.gearStats, dodgeRating: 1_000_000, meleeWeapon: { damageMin: 100, damageMax: 100, damageSchool: "physical", speedMs: 2_000, }, }, talentRanks: { ...state.talentRanks, "talent-2073": rank }, })); useCombatStore.getState().registerMob(id, { name: id, level: 80, maxHealth: 100_000, xpReward: 0, attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: attackId, initialCooldownMs: [0, 0], repeatCooldownMs: [repeatCooldownMs, repeatCooldownMs], }], }); setMobPosition(id, [2, 0, 0]); useCombatStore.getState().damageMob(id, 1, 1_100); return attackId; }; it.each([[1, 0.5], [2, 1]] as const)( "returns the generated rank %i main-hand damage fraction after a dodge", (rank, multiplier) => { const targetId = `unfair-advantage-rank-${rank}`; const attackId = setUpDodgeTarget(targetId, rank); const state = useCombatStore.getState(); const weapon = state.gearStats.meleeWeapon!; const stats = deriveCharacterStats( state.classId, state.level, state.gearStats, state.raceId, {}, state.secondaryClassId, ); const expectedRawDamage = Math.round(( (weapon.damageMin + weapon.damageMax) / 2 + stats.attackPower / 14 * weapon.speedMs / 1_000 ) * multiplier); const dodgeAt = timeForDodge(targetId, attackId, 0, 1_450); useCombatStore.getState().advanceMobCombat(dodgeAt); const retaliation = useCombatStore.getState().combatEvents.find((event) => ( event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID && event.targetActorId === targetId && event.abilityId === WOW335_ROGUE_UNFAIR_ADVANTAGE_DAMAGE_ID )); expect(useCombatStore.getState().combatEvents.some((event) => ( event.kind === "damage" && event.sourceActorId === targetId && event.targetActorId === PLAYER_AGGRO_ID && event.outcome === "dodge" ))).toBe(true); expect(retaliation?.rawAmount).toBe(expectedRawDamage); expect(wow335RogueUnfairAdvantageDamageMultiplierForRank(rank)).toBe(multiplier); }, ); it("shares one two-second cooldown across repeated dodges", () => { const targetId = "unfair-advantage-cooldown-target"; const attackId = setUpDodgeTarget(targetId, 2, 500); const procTimes: number[] = []; let attackAt = timeForDodge(targetId, attackId, 0, 1_450); for (let attackRevision = 0; attackRevision < 12; attackRevision += 1) { useCombatStore.getState().advanceMobCombat(attackAt); const procs = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.abilityId === WOW335_ROGUE_UNFAIR_ADVANTAGE_DAMAGE_ID )); if (procs.length > procTimes.length) procTimes.push(attackAt); attackAt = timeForDodge(targetId, attackId, attackRevision + 1, attackAt + 500); if (procTimes.length === 2) break; } expect(procTimes).toHaveLength(2); expect(procTimes[1]! - procTimes[0]!).toBeGreaterThanOrEqual(2_000); }); it("uses a companion Rogue's rank, melee weapon, and individual cooldown on dodge", () => { const playerHealth = useCombatStore.getState().health; usePartyStore.getState().initializeParty("unfair-advantage-companion", 80, playerHealth, "damage", 2); const initialMember = usePartyStore.getState().members[0]!; const memberId = initialMember.id; usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === memberId ? { ...member, classId: "rogue", talentRanks: { ...member.talentRanks, "talent-2073": 2 }, gearStats: { ...member.gearStats, dodgeRating: 1_000_000, meleeWeapon: { damageMin: 100, damageMax: 100, damageSchool: "physical" as const, speedMs: 2_000, }, }, } : member), })); registerPartyRuntimePosition(memberId, [1, 0, 0]); const targetId = "unfair-advantage-companion-target"; const attackId = `${targetId}-swing`; useCombatStore.getState().registerMob(targetId, { name: targetId, level: 80, maxHealth: 100_000, xpReward: 0, attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: attackId, initialCooldownMs: [0, 0], repeatCooldownMs: [3_000, 3_000], }], }); setMobPosition(targetId, [2, 0, 0]); const member = usePartyStore.getState().members.find((candidate) => candidate.id === memberId)!; useCombatStore.getState().damageMob(targetId, 1, 1_100, { actorId: memberId, role: member.role, abilityId: "test-companion-engage", }); const dodgeAt = timeForDodge(targetId, attackId, 0, 1_450, memberId); useCombatStore.getState().advanceMobCombat(dodgeAt); const retaliation = useCombatStore.getState().combatEvents.find((event) => ( event.kind === "damage" && event.sourceActorId === memberId && event.targetActorId === targetId && event.abilityId === WOW335_ROGUE_UNFAIR_ADVANTAGE_DAMAGE_ID )); expect(useCombatStore.getState().combatEvents.some((event) => ( event.kind === "damage" && event.sourceActorId === targetId && event.targetActorId === memberId && event.outcome === "dodge" ))).toBe(true); expect(retaliation?.rawAmount).toBeGreaterThan(0); expect(usePartyStore.getState().members.find((candidate) => candidate.id === memberId)?.cooldowns[ WOW335_ROGUE_UNFAIR_ADVANTAGE_DAMAGE_ID ]).toBe(dodgeAt + 2_000); }); });