Files
healer-man/src/game/wow335DeathKnightSpellDeflection.integration.test.ts
T
2026-10-02 10:38:12 -04:00

141 lines
5.5 KiB
TypeScript

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