Update Healer Man server

This commit is contained in:
phenom committed 2026-09-30 09:57:13 -04:00
1 parent 35dfdaed61
commit 4fb7a0b9c8
161 files changed
+24618 -529

No files matched your search

+241 -8
View File
@@ -1,13 +1,20 @@
import { beforeEach, describe, expect, it } from "vitest";
import { abilityTalentNodeId, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { applyAura } from "./combatAuras";
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { cooldownResetCandidates } from "./cooldownReset";
import { runtimeTalentNodeById } from "./talentRuntimeCatalog";
import { talentModifiersForRanks } from "./talentModifiers";
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
import {
lockAndLoadRankMechanics,
WOW335_HUNTER_LOCK_AND_LOAD_ICD_TAG,
WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG,
} from "./wow335HunterRuntime";
import { usePartyStore } from "./partyStore";
describe("WoW 3.3.5 Hunter Readiness", () => {
@@ -96,6 +103,15 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
);
});
it("reads Lock and Load chance, charges, and cooldown from each generated rank description", () => {
expect([1, 2, 3].map(lockAndLoadRankMechanics)).toEqual([
expect.objectContaining({ trapProcChance: 0.33, periodicProcChance: 0.1, freeCastCount: 2, durationMs: 15_000, internalCooldownMs: 15_000 }),
expect.objectContaining({ trapProcChance: 0.66, periodicProcChance: 0.2, freeCastCount: 2, durationMs: 15_000, internalCooldownMs: 15_000 }),
expect.objectContaining({ trapProcChance: 1, periodicProcChance: 0.3, freeCastCount: 2, durationMs: 15_000, internalCooldownMs: 15_000 }),
]);
expect(lockAndLoadRankMechanics(0)).toBeNull();
});
it("resets Hunter cooldowns while leaving Bestial Wrath's cooldown untouched", () => {
const readinessId = unlockHunterTalent("Readiness");
const bestialWrathId = unlockHunterTalent("Bestial Wrath");
@@ -156,6 +172,14 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
it("places Bestial Wrath on the pet and applies the rank-two Beast Within payload to the hunter", () => {
const callPetId = hunterAbilityId("Call Pet");
const bestialWrathId = unlockHunterTalent("Bestial Wrath");
const beastWithin = useCombatStore.getState().abilities.find((candidate) => candidate.name === "The Beast Within");
const beastWithinTalent = runtimeTalentNodeById("talent-1803");
expect(beastWithinTalent).toBeDefined();
expect(beastWithin).toMatchObject({
talentNodeId: "talent-1803",
description: beastWithinTalent?.rankDescriptions[0],
});
expect(beastWithin?.ranks?.map((rank) => rank.description)).toEqual(beastWithinTalent?.rankDescriptions);
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1803": 2 },
}));
@@ -177,10 +201,11 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
const beastWithinAuras = auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.id.includes(":34471:"));
expect(beastWithinAuras).toHaveLength(3);
expect(beastWithinAuras.every((aura) => aura.expiresAt === 21_000)).toBe(true);
expect(beastWithinAuras.every((aura) => aura.expiresAt === 9_000)).toBe(true);
expect(beastWithinAuras.every((aura) => aura.definition.description === beastWithinTalent?.rankDescriptions[1])).toBe(true);
expect(beastWithinAuras.flatMap((aura) => aura.definition.modifiers ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "resource", resource: "power", property: "cost", value: -0.2 }),
expect.objectContaining({ kind: "damage", direction: "dealt", value: 0.1 }),
expect.objectContaining({ kind: "resource", resource: "power", property: "cost", value: -0.5 }),
expect.objectContaining({ kind: "damage", direction: "dealt", value: 0.15 }),
]));
});
@@ -205,8 +230,14 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
}));
const procAt = hunterProcTime(bestialWrathId, pet!.id, true, noProcAt + 5_000);
expect(useCombatStore.getState().castAbility(bestialWrathId, undefined, procAt).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.id.includes(":34471:"))).toBe(true);
const beastWithinAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.id.includes(":34471:"));
expect(beastWithinAuras).toHaveLength(3);
expect(beastWithinAuras.every((aura) => aura.expiresAt === procAt + 6_000)).toBe(true);
expect(beastWithinAuras.flatMap((aura) => aura.definition.modifiers ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "resource", resource: "power", property: "cost", value: -0.5 }),
expect.objectContaining({ kind: "damage", direction: "dealt", value: 0.15 }),
]));
});
it("stores Intimidation on the pet and spends it on the pet's next melee hit", () => {
@@ -278,7 +309,7 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
))).toBe(false);
});
it("does not consume Kill Command's special-attack charges on a generic pet melee swing", () => {
it("keeps Kill Command's pet aura inert on generic melee swings", () => {
registerHunterMob("kill-command-target", [0, 0, 3]);
const callPetId = hunterAbilityId("Call Pet");
const killCommandId = hunterAbilityId("Kill Command");
@@ -292,12 +323,14 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
entry.definition.utilityTags?.includes("wow335-hunter-kill-command")
));
expect(aura?.targetId).toBe(pet!.id);
expect(aura?.stacks).toBe(60);
expect(aura?.expiresAt).toBe(33_000);
expect(aura?.definition.maxStacks).toBeUndefined();
expect(aura?.definition.initialStacks).toBeUndefined();
expect(aura?.definition.modifiers?.some((modifier) => modifier.kind === "damage") ?? false).toBe(false);
useCombatStore.getState().tick(1, 4_000);
expect(useCombatStore.getState().auras.find((entry) => entry.instanceId === aura?.instanceId)?.stacks).toBe(60);
expect(useCombatStore.getState().auras.find((entry) => entry.instanceId === aura?.instanceId)?.expiresAt).toBe(33_000);
});
it("arms Freezing Arrow at its ground point and freezes only an approaching target", () => {
@@ -328,6 +361,206 @@ describe("WoW 3.3.5 Hunter pet mechanics", () => {
expect(useCombatStore.getState().mobs["freezing-target"]?.statuses).toHaveLength(0);
});
it("procs Lock and Load when Freezing Arrow successfully traps a target", () => {
registerHunterMob("lock-and-load-trap-target", [0, 0, 10]);
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1306": 3 },
}));
const freezingArrowId = hunterAbilityId("Freezing Arrow");
expect(useCombatStore.getState().castAbility(freezingArrowId, {
origin: [0, 0, 0], groundPosition: [0, 0, 0], now: 1_000,
})).toMatchObject({ ok: true });
setMobPosition("lock-and-load-trap-target", [0, 0, 1]);
useCombatStore.getState().tick(1, 2_000);
expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([
expect.objectContaining({
targetId: PLAYER_AURA_ENTITY_ID,
stacks: 2,
expiresAt: 17_000,
definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG]),
}),
}),
expect.objectContaining({
targetId: PLAYER_AURA_ENTITY_ID,
expiresAt: 17_000,
definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_HUNTER_LOCK_AND_LOAD_ICD_TAG]),
}),
}),
]));
});
it("uses the generated periodic-damage chance for Black Arrow ticks", () => {
const targetId = "lock-and-load-black-arrow-target";
registerHunterMob(targetId, [0, 0, 12]);
const blackArrowId = unlockHunterTalent("Black Arrow");
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1306": 3 },
}));
const firstProcTickAt = Array.from({ length: 60_000 }, (_, index) => 4_000 + index)
.find((tickAt) => hunterProcRoll(
`${PLAYER_AGGRO_ID}:${blackArrowId}:periodic:${targetId}:${tickAt}:wow335-hunter-lock-and-load`,
) < 0.3);
expect(firstProcTickAt).toBeDefined();
const castAt = firstProcTickAt! - 3_000;
expect(useCombatStore.getState().castAbility(blackArrowId, { targetActorId: targetId, now: castAt }))
.toMatchObject({ ok: true });
useCombatStore.getState().tick(1, firstProcTickAt!);
expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([
expect.objectContaining({
targetId: PLAYER_AURA_ENTITY_ID,
stacks: 2,
expiresAt: firstProcTickAt! + 15_000,
definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG]),
}),
}),
expect.objectContaining({
targetId: PLAYER_AURA_ENTITY_ID,
expiresAt: firstProcTickAt! + 15_000,
definition: expect.objectContaining({
utilityTags: expect.arrayContaining([WOW335_HUNTER_LOCK_AND_LOAD_ICD_TAG]),
}),
}),
]));
});
it("makes the next two Arcane Shots free and cooldown-free, then restores normal costs", () => {
registerHunterMob("lock-and-load-shot-target", [0, 0, 12]);
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1306": 3 },
resource: 0,
resourcePools: { ...state.resourcePools, mana: 0 },
}));
const arcaneShotId = hunterAbilityId("Arcane Shot");
const arcaneShot = useCombatStore.getState().abilities.find((ability) => ability.id === arcaneShotId)!;
const readyAura = {
kind: "apply-aura" as const,
recipient: "caster" as const,
sourceEffectType: -1,
sourceAuraType: -1,
miscValue: 0,
triggerSpellId: 0,
aura: {
id: "wow335:hunter:lock-and-load-ready",
name: "Lock and Load",
description: "Two Arcane Shot or Explosive Shot casts cost no mana and trigger no cooldown.",
disposition: "buff" as const,
durationMs: 15_000,
maxStacks: 2,
initialStacks: 2,
stackBehavior: "replace" as const,
utilityTags: [WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG],
},
};
useCombatStore.setState((state) => ({
auras: applyAura(state.auras, {
definition: readyAura.aura,
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras,
}));
useCombatStore.setState((state) => ({
cooldowns: { ...state.cooldowns, [arcaneShotId]: 30_000 },
}));
expect(useCombatStore.getState().castAbility(arcaneShotId, { targetActorId: "lock-and-load-shot-target", now: 1_000 }))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().resourcePools.mana).toBe(0);
expect(useCombatStore.getState().cooldowns[arcaneShotId]).toBe(0);
expect(useCombatStore.getState().auras.find((aura) => (
aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG)
))?.stacks).toBe(1);
useCombatStore.setState((state) => ({
cooldowns: { ...state.cooldowns, [arcaneShotId]: 40_000 },
}));
expect(useCombatStore.getState().castAbility(arcaneShotId, { targetActorId: "lock-and-load-shot-target", now: 3_000 }))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().resourcePools.mana).toBe(0);
expect(useCombatStore.getState().cooldowns[arcaneShotId]).toBe(0);
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG)
))).toBe(false);
useCombatStore.setState((state) => ({
cooldowns: { ...state.cooldowns, [arcaneShotId]: 0 },
}));
expect(useCombatStore.getState().castAbility(arcaneShotId, { targetActorId: "lock-and-load-shot-target", now: 5_000 }))
.toMatchObject({ ok: false, reason: "not-enough-resource" });
});
it("does not consume Lock and Load when an eligible shot cannot find a target", () => {
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1306": 3 },
resource: 0,
resourcePools: { ...state.resourcePools, mana: 0 },
}));
const arcaneShotId = hunterAbilityId("Arcane Shot");
const arcaneShot = useCombatStore.getState().abilities.find((ability) => ability.id === arcaneShotId)!;
useCombatStore.setState((state) => ({
auras: applyAura(state.auras, {
definition: {
id: "wow335:hunter:lock-and-load-ready", name: "Lock and Load", disposition: "buff",
durationMs: 15_000, maxStacks: 2, initialStacks: 2, stackBehavior: "replace",
utilityTags: [WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras,
}));
useCombatStore.setState((state) => ({
cooldowns: { ...state.cooldowns, [arcaneShotId]: 30_000 },
}));
expect(useCombatStore.getState().castAbility(arcaneShotId, { now: 1_000 }))
.toMatchObject({ ok: false, reason: "no-target" });
expect(useCombatStore.getState().cooldowns[arcaneShotId]).toBe(30_000);
expect(useCombatStore.getState().auras.find((aura) => (
aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG)
))?.stacks).toBe(2);
});
it("spends a Lock and Load charge on a cast that misses", () => {
const targetId = "lock-and-load-miss-target";
registerHunterMob(targetId, [0, 0, 12]);
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1306": 3 },
}));
const arcaneShotId = hunterAbilityId("Arcane Shot");
const arcaneShot = useCombatStore.getState().abilities.find((ability) => ability.id === arcaneShotId)!;
useCombatStore.setState((state) => ({
auras: applyAura(state.auras, {
definition: {
id: "wow335:hunter:lock-and-load-ready", name: "Lock and Load", disposition: "buff",
durationMs: 15_000, maxStacks: 2, initialStacks: 2, stackBehavior: "replace",
utilityTags: [WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras,
}));
const missAt = Array.from({ length: 13_000 }, (_, index) => 3_000 + index).find((now) => (
hunterProcRoll(`${PLAYER_AGGRO_ID}:${arcaneShot.id}:${targetId}:${now}:direct:outcome`) < 0.05
));
expect(missAt).toBeDefined();
expect(useCombatStore.getState().castAbility(arcaneShotId, { targetActorId: targetId, now: missAt! }))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().lastCombatResult?.outcome).toBe("miss");
expect(useCombatStore.getState().auras.find((aura) => (
aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG)
))?.stacks).toBe(1);
});
it("arms Explosive Trap, then deals its rank fire impact and periodic AoE ticks", () => {
registerHunterMob("explosive-center", [0, 0, 0]);
useCombatStore.getState().registerMob("explosive-edge", { name: "explosive-edge", maxHealth: 100_000, xpReward: 0 });