forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user