Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
@@ -2,7 +2,7 @@ import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { equipmentAdjustedCombatAmount, PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { installCombatClock } from "./combatClock";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { companionResourceMaximum } from "./combatActorAuthority";
|
||||
@@ -22,7 +22,7 @@ function finishCast(now: number) {
|
||||
if (cast) useCombatStore.getState().tick((cast.completesAt - now) / 1_000 + 0.01, cast.completesAt);
|
||||
}
|
||||
|
||||
function prepareDruid(secondaryClassId?: "rom-warden" | "rom-scout") {
|
||||
function prepareDruid(secondaryClassId?: "rom-warden" | "rom-scout" | "rom-mage") {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-druid",
|
||||
...(secondaryClassId ? { secondaryClassId } : {}),
|
||||
@@ -37,9 +37,25 @@ function prepareDruid(secondaryClassId?: "rom-warden" | "rom-scout") {
|
||||
}));
|
||||
}
|
||||
|
||||
function prepareMageDruid() {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-mage",
|
||||
secondaryClassId: "rom-druid",
|
||||
raceId: "rom-elf",
|
||||
level: 80,
|
||||
});
|
||||
useCombatStore.setState((state) => ({
|
||||
health: 1,
|
||||
resource: 1_000,
|
||||
maxResource: 1_000,
|
||||
resourcePools: { ...state.resourcePools, mana: 1_000 },
|
||||
maxResourcePools: { ...state.maxResourcePools, mana: 1_000 },
|
||||
}));
|
||||
}
|
||||
|
||||
describe("RuneWaker Druid runtime", () => {
|
||||
beforeAll(async () => {
|
||||
await preloadAbilityCatalogsForClasses(["rom-druid"]);
|
||||
await preloadAbilityCatalogsForClasses(["rom-druid", "rom-mage"]);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -136,6 +152,77 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(companionResourceMaximum(companion, "nature-power")).toBe(15);
|
||||
});
|
||||
|
||||
it("converts exactly 10% MP into four Nature Power with Essence Extraction", () => {
|
||||
prepareDruid("rom-warden");
|
||||
const essence = ability(494373);
|
||||
expect(essence.costs).toContainEqual({ resource: "mana", amount: 10, percentage: true });
|
||||
expect(useCombatStore.getState().resourcePools).toMatchObject({ mana: 1_000, "nature-power": 0 });
|
||||
|
||||
expect(useCombatStore.getState().castAbility(essence.id, { now: 1_000 }).ok).toBe(true);
|
||||
expect(useCombatStore.getState().resourcePools.mana).toBe(900);
|
||||
expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(4);
|
||||
});
|
||||
|
||||
it("applies Grace of the Forest's Nature Power bonus only when a point is held", () => {
|
||||
prepareDruid("rom-warden");
|
||||
const grace = ability(494937);
|
||||
expect(useCombatStore.getState().castAbility(grace.id, { now: 1_000 }).ok).toBe(true);
|
||||
const baseAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID);
|
||||
expect(baseAuras.some((aura) => aura.definition.modifiers?.some((modifier) => modifier.kind === "resource"
|
||||
&& modifier.resource === "mana" && modifier.property === "maximum"
|
||||
&& modifier.operation === "percent" && modifier.value === 0.1))).toBe(false);
|
||||
expect(baseAuras.some((aura) => aura.definition.modifiers?.some((modifier) => modifier.kind === "healing"
|
||||
&& modifier.direction === "done" && modifier.operation === "percent" && modifier.value === 0.03))).toBe(true);
|
||||
|
||||
prepareDruid("rom-warden");
|
||||
useCombatStore.setState((state) => ({
|
||||
resourcePools: { ...state.resourcePools, "nature-power": 1 },
|
||||
}));
|
||||
expect(useCombatStore.getState().castAbility(grace.id, { now: 2_000 }).ok).toBe(true);
|
||||
const poweredAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID);
|
||||
expect(poweredAuras.find((aura) => aura.definition.modifiers?.some((modifier) => modifier.kind === "resource"
|
||||
&& modifier.resource === "mana" && modifier.property === "maximum"
|
||||
&& modifier.operation === "percent" && modifier.value === 0.1))?.expiresAt).toBe(6_000);
|
||||
expect(poweredAuras.find((aura) => aura.definition.id === "rom-druid-grace-of-the-forest-powered-healing")?.expiresAt)
|
||||
.toBe(6_000);
|
||||
});
|
||||
|
||||
it("selects and applies the source Seed defense branch to its target", () => {
|
||||
const branches = [
|
||||
{ spellId: 493546, baseComponentId: 503815, poweredComponentId: 620458, stat: "magical-defense" },
|
||||
{ spellId: 493545, baseComponentId: 503814, poweredComponentId: 620457, stat: "armor" },
|
||||
] as const;
|
||||
|
||||
branches.forEach(({ spellId, baseComponentId, poweredComponentId, stat }, index) => {
|
||||
for (const { naturePower, componentId } of [
|
||||
{ naturePower: 0, componentId: baseComponentId },
|
||||
{ naturePower: 1, componentId: poweredComponentId },
|
||||
]) {
|
||||
const targetId = `${TARGET_ID}-seed-${spellId}-${naturePower}`;
|
||||
prepareDruid();
|
||||
useCombatStore.setState((state) => ({
|
||||
resourcePools: { ...state.resourcePools, "nature-power": naturePower },
|
||||
}));
|
||||
useCombatStore.getState().registerMob(targetId, { name: "Seed Target", maxHealth: 10_000 });
|
||||
setMobPosition(targetId, [5, 0, 0]);
|
||||
|
||||
const seed = ability(spellId);
|
||||
const now = 2_000 + index * 4_000 + naturePower * 1_000;
|
||||
expect(useCombatStore.getState().castAbility(seed.id, { targetActorId: targetId, now }).ok).toBe(true);
|
||||
expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(1);
|
||||
const seedAura = useCombatStore.getState().auras.find((aura) => aura.targetId === targetId
|
||||
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === stat));
|
||||
expect(seedAura).toMatchObject({
|
||||
expiresAt: now + 20_000,
|
||||
definition: expect.objectContaining({
|
||||
id: expect.stringContaining(`-${componentId}-`),
|
||||
modifiers: [expect.objectContaining({ kind: "stat", stat, operation: "percent", value: -0.02 })],
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("adds exactly one percent of each healing target's max health to the Spirit of Life tick", () => {
|
||||
expect(romDruidPeriodicHealAmount(500, 10_000, 0.01)).toBe(600);
|
||||
expect(romDruidPeriodicHealAmount(500, 5_000, 0.01)).toBe(550);
|
||||
@@ -185,4 +272,178 @@ describe("RuneWaker Druid runtime", () => {
|
||||
expect(buffs.length).toBeGreaterThanOrEqual(2);
|
||||
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", () => {
|
||||
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;
|
||||
const protectionCast = useCombatStore.getState().castAbility(protection.id, {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
});
|
||||
expect(protectionCast.ok).toBe(true);
|
||||
const taggedProtection = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.definition.utilityTags?.includes("rom-druid-mother-earth-protection")
|
||||
));
|
||||
expect(taggedProtection?.expiresAt).toBe(6_000);
|
||||
|
||||
const startingHealth = useCombatStore.getState().health;
|
||||
useCombatStore.getState().tick(0, 5_999);
|
||||
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);
|
||||
});
|
||||
|
||||
it("does not trigger Green Guardian for another pair or another aura caster", () => {
|
||||
prepareDruid("rom-mage");
|
||||
const protection = ability(493533);
|
||||
const wrongPairPassive = useCombatStore.getState().abilities.find((entry) => entry.dbcSpellId === 494331);
|
||||
expect(wrongPairPassive).toBeUndefined();
|
||||
expect(useCombatStore.getState().castAbility(protection.id, {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
}).ok).toBe(true);
|
||||
useCombatStore.getState().tick(1, 6_000);
|
||||
expect(useCombatStore.getState().effects.filter((effect) => effect.kind === "hot")).toHaveLength(0);
|
||||
|
||||
prepareMageDruid();
|
||||
const passive = ability(494331);
|
||||
expect(useCombatStore.getState().castAbility(protection.id, {
|
||||
targetActorId: null,
|
||||
now: 10_000,
|
||||
}).ok).toBe(true);
|
||||
const taggedProtection = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.definition.utilityTags?.includes("rom-druid-mother-earth-protection")
|
||||
));
|
||||
expect(taggedProtection).toBeDefined();
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: state.auras.map((aura) => aura.instanceId === taggedProtection!.instanceId
|
||||
? { ...aura, sourceId: "another-caster" }
|
||||
: aura),
|
||||
}));
|
||||
useCombatStore.getState().tick(1, taggedProtection!.expiresAt!);
|
||||
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", () => {
|
||||
prepareMageDruid();
|
||||
usePartyStore.getState().initializeParty("green-guardian-recipient", 80, 1, "damage", 5, "rom");
|
||||
const recipient = usePartyStore.getState().members[0]!;
|
||||
usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === recipient.id
|
||||
? { ...member, health: 1 }
|
||||
: member) }));
|
||||
registerPartyRuntimePosition(recipient.id, [4, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
|
||||
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,
|
||||
}).ok).toBe(true);
|
||||
const protectionAura = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.sourceId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.targetId === recipient.id
|
||||
&& aura.definition.utilityTags?.includes("rom-druid-mother-earth-protection")
|
||||
));
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a party Mage/Druid's Green Guardian when their protection expires", () => {
|
||||
prepareMageDruid();
|
||||
const caster = createConfiguredPartyMember({
|
||||
name: "Mage Druid",
|
||||
classId: "rom-mage",
|
||||
secondaryClassId: "rom-druid",
|
||||
specialization: "Mage",
|
||||
specializationId: "rom-mage-druid",
|
||||
talentTreeId: "rom-mage-druid",
|
||||
role: "damage",
|
||||
}, 80, 0, "green-guardian-companion");
|
||||
usePartyStore.getState().initializeParty("green-guardian-companion", 80,
|
||||
useCombatStore.getState().health, "damage", 2, "rom");
|
||||
usePartyStore.setState({ members: [caster] });
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
|
||||
const passive = ability(494331);
|
||||
const protection = ability(493533);
|
||||
const protectionEffect = protection.effects.find((effect) => (
|
||||
effect.kind === "apply-aura"
|
||||
&& effect.aura.utilityTags?.includes("rom-druid-mother-earth-protection")
|
||||
));
|
||||
if (!protectionEffect || protectionEffect.kind !== "apply-aura") {
|
||||
throw new Error("Mother Earth's Protection did not expose its tagged aura effect.");
|
||||
}
|
||||
expect(useCombatStore.getState().applyActorAura(
|
||||
caster.id,
|
||||
PLAYER_AURA_ENTITY_ID,
|
||||
protection,
|
||||
protectionEffect,
|
||||
1_000,
|
||||
)).toBe(true);
|
||||
|
||||
useCombatStore.getState().tick(0, 6_000);
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user