Update Healer Man server
This commit is contained in:
1 parent
4c768fbec6
commit
b16821e92e
330 files changed
+35406
-1760
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { abilityById, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { equipmentAdjustedCombatAmount, PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { installCombatClock } from "./combatClock";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { companionResourceMaximum } from "./combatActorAuthority";
|
||||
@@ -163,6 +163,73 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(4);
|
||||
});
|
||||
|
||||
it("casts the current Druid/Rogue Necrotic Wound only with poison and Nature Power, then spreads three hits", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-druid",
|
||||
secondaryClassId: "rom-rogue",
|
||||
raceId: "rom-elf",
|
||||
level: 80,
|
||||
});
|
||||
useCombatStore.setState((state) => ({
|
||||
resource: 1_000,
|
||||
maxResource: 1_000,
|
||||
resourcePools: { ...state.resourcePools, mana: 1_000, energy: 100, "nature-power": 1 },
|
||||
maxResourcePools: { ...state.maxResourcePools, mana: 1_000, energy: 100 },
|
||||
cooldowns: { ...state.cooldowns, [ability(494023).id]: 11_000 },
|
||||
}));
|
||||
const primary = "rom-druid-wound-primary";
|
||||
const nearby = "rom-druid-wound-nearby";
|
||||
const outside = "rom-druid-wound-outside";
|
||||
for (const [id, position] of [
|
||||
[primary, [4, 0, 0]],
|
||||
[nearby, [11, 0, 0]],
|
||||
[outside, [13, 0, 0]],
|
||||
] as const) {
|
||||
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000_000, xpReward: 0 });
|
||||
setMobPosition(id, position);
|
||||
useCombatStore.getState().damageMob(id, 1, 100);
|
||||
}
|
||||
|
||||
const wound = ability(494321);
|
||||
const poison = ability(494024);
|
||||
const runtimePoison = abilityById(poison.id);
|
||||
expect(runtimePoison).toMatchObject({
|
||||
id: poison.id,
|
||||
dbcSpellId: 494024,
|
||||
effects: expect.arrayContaining([expect.objectContaining({
|
||||
kind: "dot",
|
||||
tag: "rom-druid-corrosive-poison",
|
||||
})]),
|
||||
});
|
||||
expect(useCombatStore.getState().castAbility(wound.id, { targetActorId: primary, now: 1_000 }))
|
||||
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
||||
expect(useCombatStore.getState().castAbility(poison.id, { targetActorId: primary, now: 2_000 }).ok).toBe(true);
|
||||
useCombatStore.setState((state) => ({
|
||||
resourcePools: { ...state.resourcePools, "nature-power": 0 },
|
||||
}));
|
||||
expect(useCombatStore.getState().castAbility(wound.id, { targetActorId: primary, now: 3_500 }))
|
||||
.toMatchObject({ ok: false, reason: "not-enough-resource" });
|
||||
useCombatStore.setState((state) => ({
|
||||
resourcePools: { ...state.resourcePools, "nature-power": 1 },
|
||||
}));
|
||||
|
||||
expect(useCombatStore.getState().castAbility(wound.id, { targetActorId: primary, now: 5_000 }).ok).toBe(true);
|
||||
expect(useCombatStore.getState().resourcePools).toMatchObject({ mana: 960, "nature-power": 0, energy: 75 });
|
||||
finishCast(5_000);
|
||||
|
||||
const woundHits = useCombatStore.getState().combatEvents.filter((event) => (
|
||||
event.abilityId === wound.id && event.kind === "damage"
|
||||
));
|
||||
expect(woundHits).toHaveLength(6);
|
||||
const activePoisonTargets = useCombatStore.getState().effects.filter((effect) => (
|
||||
effect.kind === "dot" && effect.tag === "rom-druid-corrosive-poison" && effect.remainingTicks === 12
|
||||
)).map((effect) => effect.targetId).sort();
|
||||
expect(activePoisonTargets).toEqual([nearby, primary].sort());
|
||||
expect(useCombatStore.getState().mobs[outside]!.health).toBe(999_999);
|
||||
expect(useCombatStore.getState().cooldowns[ability(494023).id]).toBe(10_000);
|
||||
expect(useCombatStore.getState().mobs[primary]!.statuses.some((status) => status.kind === "root")).toBe(false);
|
||||
});
|
||||
|
||||
it("applies Grace of the Forest's Nature Power bonus only when a point is held", () => {
|
||||
prepareDruid("rom-warden");
|
||||
const grace = ability(494937);
|
||||
@@ -273,19 +340,13 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(buffs.every((aura) => aura.targetId === ally.id && aura.definition.disposition === "buff")).toBe(true);
|
||||
});
|
||||
|
||||
it("triggers both Green Guardian HoTs only after the caster's protection expires", () => {
|
||||
it("suppresses the stale Green Guardian HoTs when the exact-pair active skill's protection expires", () => {
|
||||
prepareMageDruid();
|
||||
const state = useCombatStore.getState();
|
||||
const passive = ability(494331);
|
||||
const protection = ability(493533);
|
||||
expect(state.abilities.some((entry) => entry.id === passive.id && entry.passive)).toBe(true);
|
||||
|
||||
const percentHot = passive.effects.find((effect) => effect.kind === "hot" && effect.percentMaxHealth !== undefined);
|
||||
const flatHot = passive.effects.find((effect) => effect.kind === "hot" && effect.percentMaxHealth === undefined);
|
||||
expect(percentHot).toMatchObject({ kind: "hot", percentMaxHealth: 0.02, ticks: 5, intervalMs: 2_000 });
|
||||
expect(flatHot).toMatchObject({ kind: "hot", basePoints: 39, pointsPerLevel: 9, ticks: 5, intervalMs: 2_000 });
|
||||
|
||||
const targetMaxHealth = state.maxHealth;
|
||||
expect(state.abilities.some((entry) => entry.id === passive.id && !entry.passive)).toBe(true);
|
||||
expect(passive.effects).toEqual([expect.objectContaining({ kind: "apply-aura" })]);
|
||||
const protectionCast = useCombatStore.getState().castAbility(protection.id, {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
@@ -301,30 +362,8 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id)).toHaveLength(0);
|
||||
expect(useCombatStore.getState().health).toBe(startingHealth);
|
||||
useCombatStore.getState().tick(0, 6_000);
|
||||
const triggered = useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id);
|
||||
expect(triggered).toHaveLength(2);
|
||||
expect(triggered.every((effect) => effect.kind === "hot" && effect.targetId === null
|
||||
&& effect.remainingTicks === 5 && effect.nextTickAt === 8_000)).toBe(true);
|
||||
expect(useCombatStore.getState().health).toBe(startingHealth);
|
||||
|
||||
const flatTickAmount = equipmentAdjustedCombatAmount(
|
||||
flatHot as Extract<NonNullable<typeof flatHot>, { kind: "hot" }>,
|
||||
80,
|
||||
state.gearStats,
|
||||
"rom-mage",
|
||||
passive,
|
||||
);
|
||||
expect(flatTickAmount).toBe(750);
|
||||
const expectedPerTick = Math.round(targetMaxHealth * 0.02 + flatTickAmount);
|
||||
const expectedTotal = expectedPerTick * 5;
|
||||
let observedTicks = 0;
|
||||
for (const now of [8_000, 10_000, 12_000, 14_000, 16_000]) {
|
||||
useCombatStore.getState().tick(0, now);
|
||||
observedTicks += 1;
|
||||
expect(useCombatStore.getState().health - startingHealth).toBe(expectedPerTick * observedTicks);
|
||||
}
|
||||
expect(useCombatStore.getState().health - startingHealth).toBe(expectedTotal);
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id)).toHaveLength(0);
|
||||
expect(useCombatStore.getState().health).toBe(startingHealth);
|
||||
});
|
||||
|
||||
it("does not trigger Green Guardian for another pair or another aura caster", () => {
|
||||
@@ -358,7 +397,7 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("heals the friendly recipient of Mother Earth's Protection when it expires", () => {
|
||||
it("suppresses stale Green Guardian HoTs on a friendly protection recipient", () => {
|
||||
prepareMageDruid();
|
||||
usePartyStore.getState().initializeParty("green-guardian-recipient", 80, 1, "damage", 5, "rom");
|
||||
const recipient = usePartyStore.getState().members[0]!;
|
||||
@@ -370,10 +409,6 @@ describe("RuneWaker Druid runtime", () => {
|
||||
|
||||
const passive = ability(494331);
|
||||
const protection = ability(493533);
|
||||
const percentHot = passive.effects.find((effect) => effect.kind === "hot" && effect.percentMaxHealth !== undefined);
|
||||
const flatHot = passive.effects.find((effect) => effect.kind === "hot" && effect.percentMaxHealth === undefined);
|
||||
const percentMaxHealth = percentHot?.kind === "hot" ? percentHot.percentMaxHealth ?? 0 : 0;
|
||||
expect(percentMaxHealth).toBe(0.02);
|
||||
expect(useCombatStore.getState().castAbility(protection.id, {
|
||||
targetActorId: recipient.id,
|
||||
now: 1_000,
|
||||
@@ -386,26 +421,11 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(protectionAura?.expiresAt).toBe(6_000);
|
||||
useCombatStore.getState().tick(0, protectionAura!.expiresAt!);
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.sourceAbilityId === passive.id
|
||||
&& effect.targetId === recipient.id)).toHaveLength(2);
|
||||
|
||||
const targetMaxHealth = recipient.maxHealth;
|
||||
const flatTickAmount = equipmentAdjustedCombatAmount(
|
||||
flatHot as Extract<NonNullable<typeof flatHot>, { kind: "hot" }>,
|
||||
80,
|
||||
useCombatStore.getState().gearStats,
|
||||
"rom-mage",
|
||||
passive,
|
||||
);
|
||||
const expectedPerTick = Math.round(targetMaxHealth * percentMaxHealth + flatTickAmount);
|
||||
for (const now of [8_000, 10_000, 12_000, 14_000, 16_000]) {
|
||||
const before = usePartyStore.getState().members.find((member) => member.id === recipient.id)!.health;
|
||||
useCombatStore.getState().tick(0, now);
|
||||
const after = usePartyStore.getState().members.find((member) => member.id === recipient.id)!.health;
|
||||
expect(after - before).toBe(expectedPerTick);
|
||||
}
|
||||
&& effect.targetId === recipient.id)).toHaveLength(0);
|
||||
expect(usePartyStore.getState().members.find((member) => member.id === recipient.id)!.health).toBe(1);
|
||||
});
|
||||
|
||||
it("uses a party Mage/Druid's Green Guardian when their protection expires", () => {
|
||||
it("suppresses stale Green Guardian HoTs for a companion's protection expiry", () => {
|
||||
prepareMageDruid();
|
||||
const caster = createConfiguredPartyMember({
|
||||
name: "Mage Druid",
|
||||
@@ -442,8 +462,7 @@ describe("RuneWaker Druid runtime", () => {
|
||||
const triggered = useCombatStore.getState().effects.filter((effect) => (
|
||||
effect.sourceAbilityId === passive.id && effect.sourceActorId === caster.id
|
||||
));
|
||||
expect(triggered).toHaveLength(2);
|
||||
expect(triggered.every((effect) => effect.kind === "hot"
|
||||
&& effect.targetId === null && effect.nextTickAt === 8_000)).toBe(true);
|
||||
expect(passive.passive).toBe(false);
|
||||
expect(triggered).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user