forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
@@ -0,0 +1,249 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { applyAura, resolveStatValue } from "./combatAuras";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
|
||||
import { preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
|
||||
const TARGET_ID = "druid-proc-target";
|
||||
|
||||
function unit(seed: string): number {
|
||||
let hash = 2166136261;
|
||||
for (let index = 0; index < seed.length; index += 1) {
|
||||
hash ^= seed.charCodeAt(index);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return (hash >>> 0) / 0x1_0000_0000;
|
||||
}
|
||||
|
||||
function abilityId(name: string): string {
|
||||
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
|
||||
expect(ability, name).toBeDefined();
|
||||
return ability!.id;
|
||||
}
|
||||
|
||||
function findDruidProcStart(name: string, startAt: number, chance: number, procTag: string): number {
|
||||
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
|
||||
expect(ability, name).toBeDefined();
|
||||
for (let start = startAt; start < startAt + 100_000; start += 100) {
|
||||
const impactAt = start + ability!.castTimeMs;
|
||||
const directSeed = `${PLAYER_AGGRO_ID}:${ability!.id}:${TARGET_ID}:${impactAt}:direct`;
|
||||
if (unit(`${directSeed}:outcome`) > 0.1 && unit(`${directSeed}:${procTag}`) < chance) return start;
|
||||
}
|
||||
throw new Error(`No deterministic ${name} proc seed found.`);
|
||||
}
|
||||
|
||||
function castAndFinish(name: string, startedAt: number) {
|
||||
const id = abilityId(name);
|
||||
const result = useCombatStore.getState().castAbility(id, { targetActorId: TARGET_ID, now: startedAt });
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
const activeCast = useCombatStore.getState().activeCast;
|
||||
if (activeCast?.abilityId === id) {
|
||||
useCombatStore.getState().tick(Math.max(0.1, (activeCast.completesAt - startedAt) / 1_000), activeCast.completesAt);
|
||||
}
|
||||
return useCombatStore.getState().combatEvents.filter((event) => (
|
||||
event.kind === "damage" && event.abilityId === id
|
||||
)).at(-1);
|
||||
}
|
||||
|
||||
describe("WoW Druid proc runtime", () => {
|
||||
beforeEach(async () => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
await preloadAbilityCatalogsForClasses(["druid"]);
|
||||
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
|
||||
useCombatStore.getState().resetSettings();
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
usePartyStore.getState().initializeParty("wow335-druid-proc-tests", 1, useCombatStore.getState().health);
|
||||
useCombatStore.setState((state) => ({
|
||||
resource: state.maxResource,
|
||||
resourcePools: { ...state.resourcePools, mana: state.maxResourcePools.mana },
|
||||
}));
|
||||
useCombatStore.getState().registerMob(TARGET_ID, { name: "Target", maxHealth: 1_000_000 });
|
||||
setMobPosition(TARGET_ID, [4, 0, 0]);
|
||||
useCombatStore.getState().selectMob(TARGET_ID);
|
||||
});
|
||||
|
||||
it("procs Solar Eclipse from Starfire and gives Wrath its source 30% damage bonus", () => {
|
||||
const baseline = castAndFinish("Wrath", 1_000);
|
||||
expect(baseline?.kind).toBe("damage");
|
||||
|
||||
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-1924": 3 } }));
|
||||
castAndFinish("Starfire", 5_000);
|
||||
const solar = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-eclipse-solar")
|
||||
));
|
||||
expect(solar?.expiresAt).toBe(23_500);
|
||||
expect(useCombatStore.getState().cooldowns["wow335-druid-eclipse-solar"]).toBe(38_500);
|
||||
|
||||
const buffed = castAndFinish("Wrath", 10_000);
|
||||
expect(buffed?.kind).toBe("damage");
|
||||
expect(buffed?.rawAmount).toBe(Math.round(baseline!.rawAmount * 1.3));
|
||||
});
|
||||
|
||||
it("procs Lunar Eclipse from Wrath and adds 30 percentage points to Starfire crit chance", () => {
|
||||
const baselineEvent = castAndFinish("Starfire", 1_000);
|
||||
expect(baselineEvent?.kind).toBe("damage");
|
||||
const baselineCritChance = useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent;
|
||||
expect(baselineCritChance).toBeDefined();
|
||||
|
||||
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-1924": 3 } }));
|
||||
const wrathStart = findDruidProcStart("Wrath", 6_000, 0.6, "druid-eclipse");
|
||||
castAndFinish("Wrath", wrathStart);
|
||||
const lunar = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-eclipse-lunar")
|
||||
));
|
||||
expect(lunar).toBeDefined();
|
||||
|
||||
castAndFinish("Starfire", wrathStart + 2_100);
|
||||
expect(useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent)
|
||||
.toBe(baselineCritChance! + 30);
|
||||
});
|
||||
|
||||
it("grants Nature's Grace after a direct healing critical and applies 20% cast haste for four seconds", () => {
|
||||
const healingTouch = abilityId("Healing Touch");
|
||||
let startedAt = 1_000;
|
||||
while (unit(`${PLAYER_AGGRO_ID}:${healingTouch}:${PLAYER_AURA_ENTITY_ID}:${startedAt + 3_000}:heal:druid-omen-of-clarity`) >= 0.06) {
|
||||
startedAt += 100;
|
||||
}
|
||||
useCombatStore.setState((state) => ({
|
||||
health: Math.round(state.maxHealth / 2),
|
||||
talentRanks: { ...state.talentRanks, "talent-789": 3, "talent-827": 3 },
|
||||
auras: [...applyAura(state.auras, {
|
||||
definition: {
|
||||
id: "test-druid-guaranteed-spell-crit",
|
||||
name: "Test Critical Chance",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
|
||||
},
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
targetId: PLAYER_AURA_ENTITY_ID,
|
||||
now: startedAt,
|
||||
}).auras],
|
||||
}));
|
||||
|
||||
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt }).ok).toBe(true);
|
||||
const firstCast = useCombatStore.getState().activeCast;
|
||||
expect(firstCast?.abilityId).toBe(healingTouch);
|
||||
useCombatStore.getState().tick(0.1, firstCast!.completesAt);
|
||||
expect(useCombatStore.getState().combatEvents.some((event) => (
|
||||
event.kind === "healing" && event.abilityId === healingTouch && event.critical
|
||||
))).toBe(true);
|
||||
|
||||
const natureGrace = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-natures-grace")
|
||||
));
|
||||
expect(natureGrace?.expiresAt).toBe(firstCast!.completesAt + 4_000);
|
||||
expect(resolveStatValue(0, [natureGrace!], "haste")).toBe(20);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
))).toBe(true);
|
||||
const manaAfterFirstHeal = useCombatStore.getState().resource;
|
||||
|
||||
const nextStart = firstCast!.completesAt + 1_500;
|
||||
useCombatStore.setState((state) => ({
|
||||
lastResourceSpentAt: { ...state.lastResourceSpentAt, mana: nextStart },
|
||||
}));
|
||||
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: nextStart }).ok).toBe(true);
|
||||
expect(useCombatStore.getState().resource).toBe(manaAfterFirstHeal);
|
||||
const hastedCast = useCombatStore.getState().activeCast;
|
||||
expect(hastedCast?.originalDurationMs).toBe(Math.round(3_000 / 1.2));
|
||||
useCombatStore.getState().tick(0.1, hastedCast!.completesAt);
|
||||
const overhealClearcasting = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
));
|
||||
expect(overhealClearcasting?.expiresAt).toBe(hastedCast!.completesAt + 15_000);
|
||||
expect(useCombatStore.getState().cooldowns["wow335-druid-omen-of-clarity"])
|
||||
.toBe(hastedCast!.completesAt + 2_000);
|
||||
});
|
||||
|
||||
it("procs Omen of Clarity from damage, makes the next eligible cast free, and respects its two-second ICD", () => {
|
||||
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-827": 3 } }));
|
||||
const moonfire = useCombatStore.getState().abilities.find((ability) => ability.name === "Moonfire");
|
||||
expect(moonfire).toBeDefined();
|
||||
const start = (() => {
|
||||
for (let now = 1_000; now < 100_000; now += 1) {
|
||||
const seed = `${PLAYER_AGGRO_ID}:${moonfire!.id}:${TARGET_ID}:${now}:direct`;
|
||||
if (unit(`${seed}:outcome`) > 0.1 && unit(`${seed}:druid-omen-of-clarity`) < 0.06) return now;
|
||||
}
|
||||
throw new Error("No deterministic Omen of Clarity proc seed found.");
|
||||
})();
|
||||
|
||||
const first = useCombatStore.getState().castAbility(moonfire!.id, { targetActorId: TARGET_ID, now: start });
|
||||
expect(first.ok, JSON.stringify(first)).toBe(true);
|
||||
const manaAfterFirst = useCombatStore.getState().resource;
|
||||
const clearcasting = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
));
|
||||
expect(clearcasting?.expiresAt).toBe(start + 15_000);
|
||||
expect(useCombatStore.getState().cooldowns["wow335-druid-omen-of-clarity"]).toBe(start + 2_000);
|
||||
|
||||
const second = useCombatStore.getState().castAbility(moonfire!.id, {
|
||||
targetActorId: TARGET_ID,
|
||||
now: start + 1_500,
|
||||
});
|
||||
expect(second.ok, JSON.stringify(second)).toBe(true);
|
||||
expect(useCombatStore.getState().resource).toBe(manaAfterFirst);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
))).toBe(false);
|
||||
});
|
||||
|
||||
it("allows a successful healing spell that fully overheals to proc Omen of Clarity", () => {
|
||||
const healingTouch = abilityId("Healing Touch");
|
||||
useCombatStore.setState((state) => ({
|
||||
health: state.maxHealth,
|
||||
talentRanks: { ...state.talentRanks, "talent-827": 3 },
|
||||
}));
|
||||
let startedAt = 1_000;
|
||||
while (unit(`${PLAYER_AGGRO_ID}:${healingTouch}:${PLAYER_AURA_ENTITY_ID}:${startedAt + 3_000}:heal:druid-omen-of-clarity`) >= 0.06) {
|
||||
startedAt += 100;
|
||||
}
|
||||
|
||||
const result = useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt });
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
const activeCast = useCombatStore.getState().activeCast;
|
||||
expect(activeCast?.abilityId).toBe(healingTouch);
|
||||
useCombatStore.getState().tick(0.1, activeCast!.completesAt);
|
||||
|
||||
const healingEvent = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "healing" && event.abilityId === healingTouch && event.occurredAt === activeCast!.completesAt
|
||||
));
|
||||
expect(healingEvent).toMatchObject({ rawAmount: expect.any(Number), effectiveAmount: 0 });
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("allows landed player auto attacks to trigger Omen of Clarity", () => {
|
||||
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, "talent-827": 3 } }));
|
||||
let now = 1_000;
|
||||
let seed = `druid-auto-attack:${now}`;
|
||||
while (unit(`${seed}:druid-omen-of-clarity`) >= 0.06) {
|
||||
now += 1;
|
||||
seed = `druid-auto-attack:${now}`;
|
||||
}
|
||||
|
||||
const result = useCombatStore.getState().damageMob(TARGET_ID, 100, now, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed,
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
});
|
||||
expect(result.amount).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
))).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user