forked from phenom/healer-man
183 lines
6.6 KiB
TypeScript
183 lines
6.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
absorbDamage,
|
|
applyAura,
|
|
dispelAuras,
|
|
evaluateAuraProcs,
|
|
expireAuras,
|
|
resolveDamageValue,
|
|
resolveResourceValue,
|
|
resolveStatValue,
|
|
type ActiveAura,
|
|
type AuraDefinition,
|
|
} from "./combatAuras";
|
|
|
|
const buff = (overrides: Partial<AuraDefinition> = {}): AuraDefinition => ({
|
|
id: "test-buff",
|
|
name: "Test Buff",
|
|
disposition: "buff",
|
|
durationMs: 5_000,
|
|
...overrides,
|
|
});
|
|
|
|
function apply(
|
|
definition: AuraDefinition,
|
|
now = 0,
|
|
current: readonly ActiveAura[] = [],
|
|
stacks = 1,
|
|
): readonly ActiveAura[] {
|
|
return applyAura(current, {
|
|
definition,
|
|
sourceId: "caster",
|
|
targetId: "player",
|
|
now,
|
|
stacks,
|
|
}).auras;
|
|
}
|
|
|
|
describe("combat auras", () => {
|
|
it("adds stacks to the cap and refreshes the duration", () => {
|
|
const definition = buff({ maxStacks: 3, stackBehavior: "add" });
|
|
let auras = apply(definition, 100, [], 2);
|
|
auras = apply(definition, 1_000, auras, 2);
|
|
|
|
expect(auras).toHaveLength(1);
|
|
expect(auras[0]).toMatchObject({ stacks: 3, appliedAt: 1_000, expiresAt: 6_000 });
|
|
});
|
|
|
|
it("refreshes without adding stacks and replaces when requested", () => {
|
|
const refreshing = buff({ maxStacks: 4, stackBehavior: "refresh" });
|
|
let auras = apply(refreshing, 0, [], 2);
|
|
auras = apply(refreshing, 500, auras, 1);
|
|
expect(auras[0]).toMatchObject({ stacks: 2, expiresAt: 5_500 });
|
|
|
|
const replacing = buff({ maxStacks: 4, stackBehavior: "replace" });
|
|
auras = apply(replacing, 1_000, auras, 1);
|
|
expect(auras[0]).toMatchObject({ stacks: 1, appliedAt: 1_000, expiresAt: 6_000 });
|
|
});
|
|
|
|
it("applies typed stat, resource, and damage modifiers", () => {
|
|
const auras = apply(buff({
|
|
maxStacks: 2,
|
|
modifiers: [
|
|
{ kind: "stat", stat: "strength", operation: "flat", value: 5 },
|
|
{ kind: "resource", resource: "mana", property: "cost", operation: "percent", value: -0.1 },
|
|
{ kind: "damage", direction: "dealt", school: "fire", operation: "percent", value: 0.2, perStack: false },
|
|
],
|
|
}), 0, [], 2);
|
|
|
|
expect(resolveStatValue(10, auras, "strength")).toBe(20);
|
|
expect(resolveResourceValue(100, auras, "mana", "cost")).toBe(80);
|
|
expect(resolveDamageValue(100, auras, { direction: "dealt", school: "fire", attackKind: "spell" })).toBe(120);
|
|
expect(resolveDamageValue(100, auras, { direction: "dealt", school: "frost", attackKind: "spell" })).toBe(100);
|
|
});
|
|
|
|
it("selectively dispels debuffs for allies and buffs for hostile purges", () => {
|
|
const magicDebuff = buff({ id: "magic-dot", disposition: "debuff", dispelCategory: "magic" });
|
|
const poisonDebuff = buff({ id: "poison", disposition: "debuff", dispelCategory: "poison" });
|
|
const magicBuff = buff({ id: "power", dispelCategory: "magic" });
|
|
let auras: readonly ActiveAura[] = [];
|
|
auras = apply(magicDebuff, 0, auras);
|
|
auras = apply(poisonDebuff, 1, auras);
|
|
auras = apply(magicBuff, 2, auras);
|
|
|
|
const cleanse = dispelAuras(auras, { relationship: "friendly", categories: ["magic"] });
|
|
expect(cleanse.removed.map((aura) => aura.definition.id)).toEqual(["magic-dot"]);
|
|
expect(cleanse.auras.map((aura) => aura.definition.id)).toEqual(["poison", "power"]);
|
|
|
|
const purge = dispelAuras(cleanse.auras, { relationship: "hostile", categories: ["magic"] });
|
|
expect(purge.removed.map((aura) => aura.definition.id)).toEqual(["power"]);
|
|
expect(purge.auras.map((aura) => aura.definition.id)).toEqual(["poison"]);
|
|
});
|
|
|
|
it("expires finite auras while retaining permanent ones", () => {
|
|
let auras: readonly ActiveAura[] = [];
|
|
auras = apply(buff({ id: "short", durationMs: 1_000 }), 100, auras);
|
|
auras = apply(buff({ id: "permanent", durationMs: null }), 100, auras);
|
|
|
|
const atBoundary = expireAuras(auras, 1_100);
|
|
expect(atBoundary.expired.map((aura) => aura.definition.id)).toEqual(["short"]);
|
|
expect(atBoundary.active.map((aura) => aura.definition.id)).toEqual(["permanent"]);
|
|
});
|
|
|
|
it("consumes only matching absorbs and reports damage left over", () => {
|
|
let auras: readonly ActiveAura[] = [];
|
|
auras = apply(buff({
|
|
id: "wards",
|
|
absorbs: [
|
|
{ id: "fire-ward", amount: 30, school: "fire" },
|
|
{ id: "barrier", amount: 25 },
|
|
],
|
|
}), 0, auras);
|
|
|
|
const result = absorbDamage(auras, {
|
|
targetId: "player",
|
|
amount: 70,
|
|
school: "fire",
|
|
now: 100,
|
|
});
|
|
expect(result).toMatchObject({ absorbed: 55, remainingDamage: 15 });
|
|
expect(result.consumptions.map(({ absorbId, amount }) => [absorbId, amount])).toEqual([
|
|
["fire-ward", 30],
|
|
["barrier", 25],
|
|
]);
|
|
expect(result.auras[0].absorbs.map((absorb) => absorb.remaining)).toEqual([0, 0]);
|
|
});
|
|
|
|
it("supports guaranteed and failed deterministic proc rolls", () => {
|
|
const definition = buff({
|
|
procs: [{
|
|
id: "spark",
|
|
triggers: "hit",
|
|
chance: 0.25,
|
|
action: { kind: "damage", amount: 10, school: "fire", target: "event-other" },
|
|
}],
|
|
});
|
|
const auras = apply(definition);
|
|
const guaranteed = evaluateAuraProcs(auras, {
|
|
trigger: "hit",
|
|
subjectId: "player",
|
|
otherId: "mob",
|
|
}, 100, () => 0.24);
|
|
expect(guaranteed.triggered).toHaveLength(1);
|
|
|
|
const failed = evaluateAuraProcs(auras, {
|
|
trigger: "hit",
|
|
subjectId: "player",
|
|
otherId: "mob",
|
|
}, 100, () => 0.25);
|
|
expect(failed.triggered).toHaveLength(0);
|
|
});
|
|
|
|
it("enforces proc internal cooldowns and consumes charges only on success", () => {
|
|
const roll = vi.fn(() => 0);
|
|
const definition = buff({
|
|
durationMs: 20_000,
|
|
procs: [{
|
|
id: "echo",
|
|
triggers: ["cast", "heal"],
|
|
chance: 1,
|
|
internalCooldownMs: 1_000,
|
|
charges: 2,
|
|
action: { kind: "custom", id: "echo-cast" },
|
|
}],
|
|
});
|
|
let auras = apply(definition);
|
|
let result = evaluateAuraProcs(auras, { trigger: "cast", subjectId: "player" }, 100, roll);
|
|
expect(result.triggered).toHaveLength(1);
|
|
expect(result.auras[0].procStates[0]).toMatchObject({ readyAt: 1_100, remainingCharges: 1 });
|
|
|
|
result = evaluateAuraProcs(result.auras, { trigger: "heal", subjectId: "player" }, 500, roll);
|
|
expect(result.triggered).toHaveLength(0);
|
|
expect(roll).toHaveBeenCalledTimes(1);
|
|
|
|
result = evaluateAuraProcs(result.auras, { trigger: "heal", subjectId: "player" }, 1_100, roll);
|
|
expect(result.triggered).toHaveLength(1);
|
|
expect(result.auras[0].procStates[0].remainingCharges).toBe(0);
|
|
|
|
result = evaluateAuraProcs(result.auras, { trigger: "cast", subjectId: "player" }, 3_000, roll);
|
|
expect(result.triggered).toHaveLength(0);
|
|
expect(roll).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|