Files
healer-man/src/game/romDruidRuntime.test.ts
T
2026-10-02 10:38:12 -04:00

469 lines
22 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import { abilityById, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { installCombatClock } from "./combatClock";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import { companionResourceMaximum } from "./combatActorAuthority";
import { romDruidPeriodicHealAmount } from "./romDruidOverrides";
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
const TARGET_ID = "rom-druid-runtime-target";
function ability(spellId: number) {
const result = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === spellId);
expect(result, `RoM Druid spell ${spellId}`).toBeDefined();
return result!;
}
function finishCast(now: number) {
const cast = useCombatStore.getState().activeCast;
if (cast) useCombatStore.getState().tick((cast.completesAt - now) / 1_000 + 0.01, cast.completesAt);
}
function prepareDruid(secondaryClassId?: "rom-warden" | "rom-scout" | "rom-mage") {
useCombatStore.getState().initializeCharacter({
classId: "rom-druid",
...(secondaryClassId ? { secondaryClassId } : {}),
raceId: "rom-elf",
level: 80,
});
useCombatStore.setState((state) => ({
resource: 1_000,
maxResource: 1_000,
resourcePools: { ...state.resourcePools, mana: 1_000, "nature-power": 0 },
maxResourcePools: { ...state.maxResourcePools, mana: 1_000 },
}));
}
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", "rom-mage"]);
});
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.setState({ playerPosition: [0, 0, 0] });
});
it("casts Spirit Guidance for one Nature Power and applies the source three-second Charm", () => {
prepareDruid();
useCombatStore.getState().registerMob(TARGET_ID, { name: "Guidance Target", maxHealth: 10_000 });
setMobPosition(TARGET_ID, [5, 0, 0]);
const spiritGuidance = ability(493536);
expect(spiritGuidance.costs).toContainEqual({ resource: "nature-power", amount: 1 });
expect(useCombatStore.getState().castAbility(spiritGuidance.id, { targetActorId: TARGET_ID, now: 1_000 }))
.toMatchObject({ ok: false, reason: "not-enough-resource" });
useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, "nature-power": 1 } }));
expect(useCombatStore.getState().castAbility(spiritGuidance.id, { targetActorId: TARGET_ID, now: 3_000 }).ok).toBe(true);
const activeCast = useCombatStore.getState().activeCast;
expect(activeCast?.abilityId).toBe(spiritGuidance.id);
finishCast(3_000);
expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(0);
expect(useCombatStore.getState().mobs[TARGET_ID]!.statuses).toContainEqual(expect.objectContaining({
kind: "charm",
endsAt: activeCast!.completesAt + 3_000,
}));
// The combat model supports the Charm control effect; it has no faction/allegiance conversion state.
});
it("applies Unity with Mother Earth as a 15-second healing and guaranteed spell-crit aura", () => {
prepareDruid();
const unity = ability(493548);
expect(unity.target).toBe("self");
expect(useCombatStore.getState().castAbility(unity.id, { now: 1_000 }).ok).toBe(true);
const aura = useCombatStore.getState().auras.find((entry) => (
entry.targetId === PLAYER_AURA_ENTITY_ID
&& entry.definition.utilityTags?.includes("rom-druid-unity-with-mother-earth")
));
expect(aura?.expiresAt).toBe(16_000);
expect(aura?.definition.modifiers).toContainEqual({
kind: "stat", stat: "magical-critical-strike", operation: "flat", value: 100,
});
expect(aura?.definition.modifiers).toContainEqual({
kind: "healing", direction: "done", operation: "percent", value: 0.1,
});
useCombatStore.getState().registerMob(TARGET_ID, { name: "Unity Target", maxHealth: 10_000 });
setMobPosition(TARGET_ID, [5, 0, 0]);
const arrow = ability(493538);
expect(useCombatStore.getState().castAbility(arrow.id, { targetActorId: TARGET_ID, now: 3_000 }).ok).toBe(true);
finishCast(3_000);
expect(useCombatStore.getState().lastCombatResult?.critical).toBe(true);
});
it("prevents movement during Unity and restores movement when its 15-second aura expires", () => {
let now = 1_000;
const restoreClock = installCombatClock(() => now);
try {
prepareDruid();
const unity = ability(493548);
expect(useCombatStore.getState().castAbility(unity.id, { now }).ok).toBe(true);
useCombatStore.getState().setPlayerPosition([6, 0, 0]);
expect(useCombatStore.getState().playerPosition).toEqual([0, 0, 0]);
now = 16_001;
useCombatStore.getState().setPlayerPosition([6, 0, 0]);
expect(useCombatStore.getState().playerPosition).toEqual([6, 0, 0]);
} finally {
restoreClock();
}
});
it("raises Nature Power's maximum to 15 only after learning Brilliance of Nature", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-druid", secondaryClassId: "rom-scout", raceId: "rom-elf", level: 44,
});
expect(useCombatStore.getState().maxResourcePools["nature-power"]).toBe(10);
useCombatStore.getState().initializeCharacter({
classId: "rom-druid", secondaryClassId: "rom-scout", raceId: "rom-elf", level: 45,
});
expect(useCombatStore.getState().abilities.some((entry) => entry.dbcSpellId === 494931 && entry.passive)).toBe(true);
expect(useCombatStore.getState().maxResourcePools["nature-power"]).toBe(15);
const companion = createConfiguredPartyMember({
name: "Druid Companion",
classId: "rom-druid",
secondaryClassId: "rom-scout",
raceId: "rom-elf",
specialization: "Primary",
role: "healer",
}, 45, 0, "rom-druid-brilliance-cap");
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("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);
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);
});
it("gives Spirit of Life a 40-second, four-second party heal within its source radius", () => {
prepareDruid("rom-warden");
usePartyStore.getState().initializeParty("rom-druid-spirit-of-life", 80, useCombatStore.getState().health, "healer", 5, "rom");
const [near, far] = usePartyStore.getState().members;
expect(near).toBeDefined();
expect(far).toBeDefined();
useCombatStore.setState({ health: 1 });
usePartyStore.setState((state) => ({ members: state.members.map((member) => ({ ...member, health: 1 })) }));
registerPartyRuntimePosition(near!.id, [5, 0, 0]);
registerPartyRuntimePosition(far!.id, [12, 0, 0]);
useCombatStore.getState().synchronizeActors();
const spirit = ability(494938);
const hot = spirit.effects.find((effect) => effect.kind === "hot");
expect(hot).toMatchObject({
ticks: 10, intervalMs: 4_000, scope: "party", radius: 7, center: "caster", percentMaxHealth: 0.01,
});
expect(spirit.effects.some((effect) => effect.kind === "summon" || effect.kind === "heal")).toBe(false);
const beforePlayer = useCombatStore.getState().health;
const beforeNear = usePartyStore.getState().members.find((member) => member.id === near!.id)!.health;
const beforeFar = usePartyStore.getState().members.find((member) => member.id === far!.id)!.health;
expect(useCombatStore.getState().castAbility(spirit.id, { now: 1_000 }).ok).toBe(true);
expect(useCombatStore.getState().summons.some((summon) => summon.sourceAbilityId === spirit.id)).toBe(false);
useCombatStore.getState().tick(4.1, 5_100);
expect(useCombatStore.getState().health).toBeGreaterThan(beforePlayer);
expect(usePartyStore.getState().members.find((member) => member.id === near!.id)!.health).toBeGreaterThan(beforeNear);
expect(usePartyStore.getState().members.find((member) => member.id === far!.id)!.health).toBe(beforeFar);
});
it("casts Savage Blessing on the selected friendly target with beneficial hit buffs", () => {
prepareDruid();
usePartyStore.getState().initializeParty("rom-druid-savage-blessing", 80, useCombatStore.getState().health, "healer", 5, "rom");
const ally = usePartyStore.getState().members[0]!;
registerPartyRuntimePosition(ally.id, [4, 0, 0]);
useCombatStore.getState().synchronizeActors();
const blessing = ability(493531);
expect(blessing.target).toBe("friendly");
expect(useCombatStore.getState().castAbility(blessing.id, { targetActorId: ally.id, now: 1_000 }).ok).toBe(true);
finishCast(1_000);
const buffs = useCombatStore.getState().auras.filter((aura) => aura.definition.name.startsWith("Savage Blessing"));
expect(buffs.length).toBeGreaterThanOrEqual(2);
expect(buffs.every((aura) => aura.targetId === ally.id && aura.definition.disposition === "buff")).toBe(true);
});
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);
expect(passive.effects).toEqual([expect.objectContaining({ kind: "apply-aura" })]);
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);
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", () => {
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("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]!;
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);
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(0);
expect(usePartyStore.getState().members.find((member) => member.id === recipient.id)!.health).toBe(1);
});
it("suppresses stale Green Guardian HoTs for a companion's protection expiry", () => {
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(passive.passive).toBe(false);
expect(triggered).toHaveLength(0);
});
});