Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
@@ -522,7 +522,7 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().shield).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps Inner Focus as an on-use, one-heal effect", () => {
|
||||
it("keeps Inner Focus as an on-use, next-spell effect", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
|
||||
const talent = TALENT_NODES.find((node) => node.name === "Inner Focus")!;
|
||||
const ranks = { [talent.id]: 1 };
|
||||
@@ -543,7 +543,7 @@ describe("combat store", () => {
|
||||
))).toBe(true);
|
||||
|
||||
expect(useCombatStore.getState().globalCooldownEndsAt).toBe(0);
|
||||
const heal = useCombatStore.getState().castAbility("wow335-priest-lesser-heal", undefined, 1_000);
|
||||
const heal = useCombatStore.getState().castAbility("wow335-priest-greater-heal", undefined, 1_000);
|
||||
expect(heal.ok).toBe(true);
|
||||
expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeGreaterThan(0);
|
||||
const completion = useCombatStore.getState().activeCast!.completesAt;
|
||||
@@ -576,6 +576,40 @@ describe("combat store", () => {
|
||||
.toMatchObject({ ok: false, reason: "global-cooldown" });
|
||||
});
|
||||
|
||||
it("reduces Inner Focus's active cooldown when Improved Flash Heal is used", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
|
||||
const talent = TALENT_NODES.find((node) => node.name === "Improved Flash Heal")!;
|
||||
const ranks = { [talent.id]: talent.maxRank };
|
||||
const innerFocus = abilitiesForClass("priest").find((ability) => ability.name === "Inner Focus")!;
|
||||
useCombatStore.setState((state) => ({
|
||||
talentRanks: ranks,
|
||||
talentModifiers: talentModifiersForRanks(ranks, "priest"),
|
||||
cooldowns: { ...state.cooldowns, [innerFocus.id]: 10_000 },
|
||||
}));
|
||||
|
||||
const cast = useCombatStore.getState().castAbility("priest-flash-heal", {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
});
|
||||
expect(cast, JSON.stringify(cast)).toMatchObject({ ok: true });
|
||||
const completion = useCombatStore.getState().activeCast!.completesAt;
|
||||
useCombatStore.getState().tick(0.1, completion);
|
||||
|
||||
expect(useCombatStore.getState().cooldowns[innerFocus.id]).toBe(8_000);
|
||||
});
|
||||
|
||||
it("converts health into mana with Life Tap", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 80, xp: 0 });
|
||||
useCombatStore.setState((state) => ({ health: 5_000, resource: 0,
|
||||
resourcePools: { ...state.resourcePools, mana: 0 } }));
|
||||
const lifeTap = useCombatStore.getState().abilities.find((ability) => ability.name === "Life Tap")!;
|
||||
|
||||
const result = useCombatStore.getState().castAbility(lifeTap.id, undefined, 1_000);
|
||||
|
||||
expect(result).toMatchObject({ ok: true, healing: 0 });
|
||||
expect(useCombatStore.getState()).toMatchObject({ health: 3_510, resource: 1_490 });
|
||||
});
|
||||
|
||||
it("ticks damage over time and awards kill experience exactly once", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 1, xp: 0 });
|
||||
registerTarget("ooze", [10, 0, 0], 5, 100);
|
||||
@@ -839,6 +873,34 @@ describe("combat store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("spreads only active Death Knight diseases with Pestilence and taunts with Death Grip", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 80, xp: 0 });
|
||||
registerTarget("disease-primary", [2, 0, 0], 5_000);
|
||||
registerTarget("disease-neighbor", [5, 0, 0], 5_000);
|
||||
useCombatStore.getState().selectMob("disease-primary");
|
||||
const abilities = useCombatStore.getState().abilities;
|
||||
const spellId = (name: string) => {
|
||||
const ability = abilities.find((entry) => entry.name === name);
|
||||
expect(ability, name).toBeDefined();
|
||||
return ability!.id;
|
||||
};
|
||||
|
||||
expect(useCombatStore.getState().castAbility(spellId("Icy Touch"), undefined, 1_000).ok).toBe(true);
|
||||
expect(useCombatStore.getState().castAbility(spellId("Plague Strike"), undefined, 3_000).ok).toBe(true);
|
||||
const neighborHealth = useCombatStore.getState().mobs["disease-neighbor"].health;
|
||||
expect(useCombatStore.getState().castAbility(spellId("Pestilence"), undefined, 5_000).ok).toBe(true);
|
||||
|
||||
expect(useCombatStore.getState().mobs["disease-neighbor"].health).toBe(neighborHealth);
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.targetId === "disease-neighbor")
|
||||
.map((effect) => effect.tag).sort()).toEqual(["blood-plague", "frost-fever"]);
|
||||
|
||||
useCombatStore.getState().selectMob("disease-neighbor");
|
||||
expect(useCombatStore.getState().castAbility(spellId("Death Grip"), undefined, 7_000).ok).toBe(true);
|
||||
expect(useCombatStore.getState().mobs["disease-neighbor"]).toMatchObject({
|
||||
forcedTarget: { actorId: PLAYER_AGGRO_ID, endsAt: 10_000 },
|
||||
});
|
||||
});
|
||||
|
||||
it("swings at the resolved companion without redirecting player damage", () => {
|
||||
const target = usePartyStore.getState().members.find((member) => member.role === "damage")!;
|
||||
usePartyStore.setState((state) => ({
|
||||
@@ -1425,6 +1487,97 @@ describe("combat store", () => {
|
||||
expect(seals[0].definition.name).toContain("Seal of Wisdom");
|
||||
});
|
||||
|
||||
it("applies Free Will Root and Slow immunity to the selected Knight/Priest ally", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-knight",
|
||||
secondaryClassId: "rom-priest",
|
||||
level: 80,
|
||||
xp: 0,
|
||||
});
|
||||
usePartyStore.getState().initializeParty("free-will-test", 80, useCombatStore.getState().health);
|
||||
const memberId = preparePartyMember(0, [2, 0, 0], Number.MAX_SAFE_INTEGER, true);
|
||||
const freeWill = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 491351);
|
||||
expect(freeWill).toBeDefined();
|
||||
|
||||
const result = useCombatStore.getState().castAbility(freeWill!.id, {
|
||||
targetActorId: memberId,
|
||||
now: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ ok: true, abilityId: freeWill!.id });
|
||||
const aura = useCombatStore.getState().auras.find((entry) => (
|
||||
entry.targetId === memberId && entry.definition.id === "rom-free-will-immunity"
|
||||
));
|
||||
expect(aura).toMatchObject({
|
||||
targetId: memberId,
|
||||
definition: {
|
||||
durationMs: 10_000,
|
||||
controlImmunities: ["root", "slow"],
|
||||
},
|
||||
});
|
||||
expect(useCombatStore.getState().auras.some((entry) => (
|
||||
entry.targetId === PLAYER_AURA_ENTITY_ID && entry.definition.id === "rom-free-will-immunity"
|
||||
))).toBe(false);
|
||||
});
|
||||
|
||||
it("fans out Crystalline Protection and Acceleration only to party members in range", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "rom-warden", level: 80, xp: 0 });
|
||||
usePartyStore.getState().initializeParty("warden-crystalline-test", 80, useCombatStore.getState().health);
|
||||
const inRangeId = preparePartyMember(0, [6, 0, 0], Number.MAX_SAFE_INTEGER);
|
||||
const outOfRangeId = preparePartyMember(1, [8, 0, 0], Number.MAX_SAFE_INTEGER);
|
||||
const abilities = useCombatStore.getState().abilities;
|
||||
const protection = abilities.find((ability) => ability.dbcSpellId === 498201)!;
|
||||
const acceleration = abilities.find((ability) => ability.dbcSpellId === 498202)!;
|
||||
|
||||
expect(useCombatStore.getState().castAbility(protection.id, { now: 1_000 }).ok).toBe(true);
|
||||
let auras = useCombatStore.getState().auras;
|
||||
expect(auras.some((entry) => entry.targetId === inRangeId && entry.definition.name === "Crystalline Protection"
|
||||
&& (entry.definition.absorbs?.length ?? 0) > 0)).toBe(true);
|
||||
expect(auras.some((entry) => entry.targetId === outOfRangeId && entry.definition.name === "Crystalline Protection"))
|
||||
.toBe(false);
|
||||
|
||||
expect(useCombatStore.getState().castAbility(acceleration.id, { now: 3_000 }).ok).toBe(true);
|
||||
auras = useCombatStore.getState().auras;
|
||||
const accelerationAuraFor = (targetId: string) => auras.filter((entry) => (
|
||||
entry.targetId === targetId && entry.definition.name.startsWith("Crystalline Acceleration")
|
||||
));
|
||||
expect(accelerationAuraFor(inRangeId).flatMap((entry) => entry.definition.modifiers ?? []))
|
||||
.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "stat", stat: "attack-speed" }),
|
||||
expect.objectContaining({ kind: "stat", stat: "spell-haste" }),
|
||||
]));
|
||||
expect(accelerationAuraFor(outOfRangeId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("applies Coat of Arms to its caster and the selected ally only", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-warden",
|
||||
secondaryClassId: "rom-warrior",
|
||||
level: 80,
|
||||
xp: 0,
|
||||
});
|
||||
usePartyStore.getState().initializeParty("warden-coat-of-arms-test", 80, useCombatStore.getState().health);
|
||||
const selectedId = preparePartyMember(0, [2, 0, 0], Number.MAX_SAFE_INTEGER, true);
|
||||
const otherId = preparePartyMember(1, [3, 0, 0], Number.MAX_SAFE_INTEGER);
|
||||
const coatOfArms = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 494611)!;
|
||||
|
||||
const result = useCombatStore.getState().castAbility(coatOfArms.id, {
|
||||
targetActorId: selectedId,
|
||||
now: 1_000,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ ok: true, abilityId: coatOfArms.id });
|
||||
const auras = useCombatStore.getState().auras.filter((entry) => entry.definition.name === "Coat of Arms");
|
||||
const hasBothDefenses = (targetId: string) => auras.some((entry) => (
|
||||
entry.targetId === targetId
|
||||
&& entry.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "armor")
|
||||
&& entry.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "magical-defense")
|
||||
));
|
||||
expect(hasBothDefenses(PLAYER_AURA_ENTITY_ID)).toBe(true);
|
||||
expect(hasBothDefenses(selectedId)).toBe(true);
|
||||
expect(hasBothDefenses(otherId)).toBe(false);
|
||||
});
|
||||
|
||||
it("redirects Hand of Sacrifice damage to the paladin", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "paladin", level: 80 });
|
||||
usePartyStore.getState().initializeParty("hand-of-sacrifice-test", 80, useCombatStore.getState().health);
|
||||
@@ -1619,15 +1772,39 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.targetId === secondId)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("requires and consumes a Rejuvenation or Regrowth HoT for Swiftmend", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "druid", level: 80, xp: 0 });
|
||||
useCombatStore.setState({ health: 1_000 });
|
||||
const swiftmend = useCombatStore.getState().abilities.find((ability) => ability.name === "Swiftmend")!;
|
||||
unlockReadySpell(swiftmend.id);
|
||||
expect(useCombatStore.getState().castAbility(swiftmend.id, { targetActorId: null, now: 1_000 }))
|
||||
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
||||
|
||||
expect(useCombatStore.getState().castAbility("druid-rejuvenation", {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
}).ok).toBe(true);
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot"
|
||||
&& effect.sourceAbilityId === "druid-rejuvenation")).toBe(true);
|
||||
|
||||
const result = useCombatStore.getState().castAbility(swiftmend.id, { targetActorId: null, now: 3_000 });
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.healing).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().health).toBeGreaterThan(1_000);
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot"
|
||||
&& effect.sourceAbilityId === "druid-rejuvenation")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps healing components of hostile abilities on the player", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 2 });
|
||||
useCombatStore.setState({ resource: 100 });
|
||||
registerTarget("death-strike-target", [2, 0, 0], 100, 0);
|
||||
registerTarget("death-strike-target", [2, 0, 0], 1_000, 0);
|
||||
const memberId = preparePartyMember(0, [2, 0, 0], 1, true);
|
||||
useCombatStore.getState().damagePlayer(40);
|
||||
const playerHealthBefore = useCombatStore.getState().health;
|
||||
expect(useCombatStore.getState().castAbility("death-knight-icy-touch", [0, 0, 0], 1_000).ok).toBe(true);
|
||||
|
||||
const result = useCombatStore.getState().castAbility("death-knight-death-strike", [0, 0, 0], 1_000);
|
||||
const result = useCombatStore.getState().castAbility("death-knight-death-strike", [0, 0, 0], 3_000);
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.damage).toBeGreaterThan(0);
|
||||
expect(result.healing).toBeGreaterThan(0);
|
||||
|
||||
Reference in new issue
Block a user