forked from phenom/healer-man
156 lines
7.5 KiB
TypeScript
156 lines
7.5 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { abilityAtLevel, abilityById } from "./abilityCatalog";
|
|
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_MAGE_FROST_WARDING_TALENT_ID,
|
|
WOW335_MAGE_FROST_WARDING_WARD_ABILITIES,
|
|
} from "./wow335MageFrostWardingRuntime";
|
|
|
|
const ATTACK = {
|
|
...DEFAULT_ENEMY_ATTACK,
|
|
id: "frost-warding-incoming-test",
|
|
name: "Frost Warding 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,
|
|
};
|
|
|
|
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`);
|
|
const wardRoll = stableCombatUnit(`${targetId}:${mobId}:${ATTACK.id}:0:wow335-frost-warding`);
|
|
if (resultRoll >= 0.2 && resultRoll < 0.9 && wardRoll < 0.5) return now;
|
|
}
|
|
throw new Error("Could not find a landed hit that passes rank 2 Frost Warding's 50% roll.");
|
|
}
|
|
|
|
function mobIdForWardProc(prefix: string, targetId: string): string {
|
|
for (let index = 0; index < 1_000; index += 1) {
|
|
const mobId = `${prefix}-${index}`;
|
|
if (stableCombatUnit(`${targetId}:${mobId}:${ATTACK.id}:0:wow335-frost-warding`) < 0.5) return mobId;
|
|
}
|
|
throw new Error("Could not find a stable Frost Warding success seed.");
|
|
}
|
|
|
|
function setupAttacker(mobId: string, targetId: string, now: number, school: EnemyDamageSchool): void {
|
|
useCombatStore.getState().registerMob(mobId, {
|
|
name: "Frost Warding 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 runPlayerHit(rank: number, ward: "fire" | "frost") {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80, xp: 0 });
|
|
usePartyStore.getState().initializeParty(`frost-warding-player-${ward}`, 80, useCombatStore.getState().health, "damage", 1);
|
|
const spell = abilityById(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES[ward].abilityId);
|
|
if (!spell) throw new Error(`Missing local Mage ${ward} ward.`);
|
|
useCombatStore.setState((state) => ({
|
|
talentRanks: { ...state.talentRanks, [WOW335_MAGE_FROST_WARDING_TALENT_ID]: rank },
|
|
resource: 10_000,
|
|
resourcePools: { ...state.resourcePools, mana: 10_000 },
|
|
}));
|
|
expect(useCombatStore.getState().castAbility(spell.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
useCombatStore.setState((state) => ({
|
|
resource: 1_000,
|
|
resourcePools: { ...state.resourcePools, mana: 1_000 },
|
|
}));
|
|
const mobId = mobIdForWardProc(`frost-warding-player-${ward}`, PLAYER_AGGRO_ID);
|
|
const now = hitTime(mobId, PLAYER_AGGRO_ID);
|
|
setupAttacker(mobId, PLAYER_AGGRO_ID, now, ward);
|
|
useCombatStore.getState().advanceMobCombat(now);
|
|
return { event: incomingEvent(mobId, PLAYER_AGGRO_ID), mana: useCombatStore.getState().resourcePools.mana ?? 0 };
|
|
}
|
|
|
|
function runCompanionHit(rank: number, ward: "fire" | "frost") {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80, xp: 0 });
|
|
usePartyStore.getState().initializeParty(`frost-warding-companion-${ward}`, 80, useCombatStore.getState().health, "damage", 1);
|
|
const talentRanks = { [WOW335_MAGE_FROST_WARDING_TALENT_ID]: rank };
|
|
const member = createConfiguredPartyMember({
|
|
name: "Frost Warding Mage", classId: "mage", specialization: "Frost",
|
|
specializationId: "mage:frost", talentTreeId: "mage-frost", talentRanks, role: "tank",
|
|
}, 80, 0, `frost-warding-companion-${ward}-${rank}`);
|
|
const wardAbility = abilityById(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES[ward].abilityId);
|
|
if (!wardAbility) throw new Error("Missing local Mage Ward ability.");
|
|
const resolvedWard = abilityAtLevel(wardAbility, 80);
|
|
const auraEffect = resolvedWard.effects.find((effect) => effect.kind === "apply-aura");
|
|
if (!auraEffect || auraEffect.kind !== "apply-aura") throw new Error("Mage Ward has no generated aura effect.");
|
|
usePartyStore.setState({ members: [{
|
|
...member,
|
|
health: 100_000,
|
|
maxHealth: 100_000,
|
|
gearStats: EMPTY_ITEM_STATS,
|
|
talentRanks,
|
|
resource: 1_000,
|
|
maxResource: 20_000,
|
|
resourcePools: { ...member.resourcePools, mana: 1_000 },
|
|
}] });
|
|
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
expect(useCombatStore.getState().applyActorAura(member.id, member.id, resolvedWard, auraEffect, 1_000)).toBe(true);
|
|
const mobId = mobIdForWardProc(`frost-warding-companion-${ward}`, member.id);
|
|
const now = hitTime(mobId, member.id);
|
|
setupAttacker(mobId, member.id, now, ward);
|
|
advanceDungeonPartyCombat(now);
|
|
useCombatStore.getState().advanceMobCombat(now);
|
|
const actor = useCombatStore.getState().actors[member.id];
|
|
return { event: incomingEvent(mobId, member.id), mana: actor?.resources.mana?.current ?? 0 };
|
|
}
|
|
|
|
describe("WoW Mage Frost Warding combat integration", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
});
|
|
|
|
it.each(["fire", "frost"] as const)(
|
|
"reduces matching %s spell damage before its Ward absorb and refunds the same Mana to player and companion",
|
|
(ward) => {
|
|
const baselinePlayer = runPlayerHit(0, ward);
|
|
const wardedPlayer = runPlayerHit(2, ward);
|
|
const baselineCompanion = runCompanionHit(0, ward);
|
|
const wardedCompanion = runCompanionHit(2, ward);
|
|
const baselineAfterAbsorb = baselinePlayer.event.effectiveAmount + baselinePlayer.event.absorbed;
|
|
const wardedAfterAbsorb = wardedPlayer.event.effectiveAmount + wardedPlayer.event.absorbed;
|
|
const companionBaselineAfterAbsorb = baselineCompanion.event.effectiveAmount + baselineCompanion.event.absorbed;
|
|
const companionWardedAfterAbsorb = wardedCompanion.event.effectiveAmount + wardedCompanion.event.absorbed;
|
|
|
|
expect(baselineAfterAbsorb).toBeGreaterThan(0);
|
|
expect(wardedAfterAbsorb).toBe(Math.round(baselineAfterAbsorb * 0.5));
|
|
expect(companionBaselineAfterAbsorb).toBeGreaterThan(0);
|
|
expect(companionWardedAfterAbsorb).toBe(Math.round(companionBaselineAfterAbsorb * 0.5));
|
|
expect(wardedPlayer.mana).toBeGreaterThan(1_000);
|
|
expect(wardedCompanion.mana).toBeGreaterThan(1_000);
|
|
},
|
|
);
|
|
});
|