Files
healer-man/src/game/wow335RogueUnfairAdvantage.test.ts
T
2026-09-30 09:57:13 -04:00

205 lines
7.4 KiB
TypeScript

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);
});
});