Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
@@ -9,6 +9,12 @@ function named(name: string) {
|
||||
return ability;
|
||||
}
|
||||
|
||||
function sourceSkill(sourceSkillId: number) {
|
||||
const ability = ROM_ALL_ABILITIES.find((entry) => entry.dbcSpellId === sourceSkillId);
|
||||
if (!ability) throw new Error(`Missing RuneWaker source skill: ${sourceSkillId}`);
|
||||
return ability;
|
||||
}
|
||||
|
||||
function isMeaningfulEffect(effect: AbilityEffect): boolean {
|
||||
if (effect.kind === "scripted" || effect.kind === "unsupported") return false;
|
||||
if (["damage", "heal", "dot", "hot", "shield"].includes(effect.kind)) {
|
||||
@@ -53,11 +59,272 @@ describe("executable RuneWaker ability mappings", () => {
|
||||
expect(named("Shout").effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "apply-aura", control: { kind: "stun" } }),
|
||||
]));
|
||||
expect(named("Shout")).toMatchObject({
|
||||
target: "self",
|
||||
radius: 10,
|
||||
targeting: { shape: "circle", radius: 10 },
|
||||
cooldownMs: 40_000,
|
||||
cost: { resource: "rage", amount: 15 },
|
||||
});
|
||||
expect(named("Cleanse").effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "dispel", mode: "cleanse", relationship: "friendly" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("keeps Priest and Priest/Scout support casts on friendly targets", () => {
|
||||
for (const name of ["Grace of Life", "Amplified Attack"]) {
|
||||
const ability = named(name);
|
||||
expect(ability.target, name).toBe("friendly");
|
||||
expect(ability.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "apply-aura", recipient: "ability-target" }),
|
||||
]));
|
||||
}
|
||||
const curingShot = named("Curing Shot");
|
||||
expect(curingShot.target).toBe("friendly");
|
||||
expect(curingShot.effects.map((effect) => effect.kind)).toEqual(expect.arrayContaining(["heal", "hot"]));
|
||||
expect(curingShot.effects.some((effect) => effect.kind === "dot")).toBe(false);
|
||||
});
|
||||
|
||||
it("fans Priest-secondary class buffs out to the described party recipients", () => {
|
||||
for (const sourceId of [491335, 498649]) {
|
||||
const auras = sourceSkill(sourceId).effects.filter((effect) => effect.kind === "apply-aura");
|
||||
expect(auras.length, `source ${sourceId}`).toBeGreaterThan(0);
|
||||
expect(auras.every((effect) => effect.scope === "party"), `source ${sourceId}`).toBe(true);
|
||||
}
|
||||
|
||||
const salvation = sourceSkill(498719).effects.filter((effect) => effect.kind === "apply-aura");
|
||||
expect(salvation.length).toBeGreaterThan(0);
|
||||
expect(salvation.every((effect) => effect.scope === "party" && effect.excludeCaster)).toBe(true);
|
||||
|
||||
const advance = sourceSkill(498722).effects.filter((effect) => effect.kind === "apply-aura");
|
||||
const partyBuffs = advance.filter((effect) => effect.scope === "party");
|
||||
const casterPenalties = advance.filter((effect) => effect.scope !== "party");
|
||||
expect(partyBuffs.some((effect) => effect.aura.modifiers?.some((modifier) => (
|
||||
modifier.kind === "damage" && modifier.direction === "dealt" && modifier.value > 0
|
||||
)))).toBe(true);
|
||||
expect(casterPenalties.some((effect) => effect.aura.modifiers?.some((modifier) => (
|
||||
modifier.kind === "stat" && ["armor", "magical-defense"].includes(modifier.stat) && modifier.value < 0
|
||||
)))).toBe(true);
|
||||
expect(casterPenalties.every((effect) => effect.recipient === "caster")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps Scout/Knight Healing Shot damage and lowest-max-health party heal separate", () => {
|
||||
const healingShot = ROM_ALL_ABILITIES.find((ability) => ability.id === "rom-scout-rom-knight-elite-20")!;
|
||||
expect(healingShot).toMatchObject({ target: "hostile" });
|
||||
expect(healingShot.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "damage" }),
|
||||
expect.objectContaining({
|
||||
kind: "heal", scope: "party", maximumTargets: 1, selection: "lowest-max-health",
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
it("requires Nature's Power for Druid/Scout Group Exorcism", () => {
|
||||
const exorcism = ROM_ALL_ABILITIES.find((ability) => ability.id === "rom-druid-rom-scout-elite-30")!;
|
||||
expect(exorcism).toMatchObject({ target: "self", targeting: { shape: "circle", radius: 5 } });
|
||||
expect(exorcism.costs).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ resource: "nature-power", amount: 1 }),
|
||||
]));
|
||||
expect(exorcism.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "dispel", mode: "cleanse", relationship: "friendly" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("roots nearby enemies with Knight Shackles of Light", () => {
|
||||
const shackles = named("Shackles of Light");
|
||||
expect(shackles).toMatchObject({ target: "self", radius: 10 });
|
||||
expect(shackles.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "apply-aura", control: { kind: "root" } }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("maps Scout Cripple Joint to its 55% Joint Blow root proc", () => {
|
||||
const crippleJoint = sourceSkill(491173);
|
||||
expect(crippleJoint.passive).toBe(true);
|
||||
expect(crippleJoint.effects).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
recipient: "caster",
|
||||
aura: expect.objectContaining({
|
||||
utilityTags: ["rom-cripple-joint"],
|
||||
procs: [{
|
||||
id: "rom-cripple-joint-root",
|
||||
triggers: ["hit", "crit"],
|
||||
chance: 0.55,
|
||||
action: { kind: "custom", id: "rom-cripple-joint" },
|
||||
}],
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(sourceSkill(490420)).toMatchObject({ name: "Joint Blow", dbcSpellId: 490420 });
|
||||
});
|
||||
|
||||
it("uses Neck Strike's sourced six-second NPC stun instead of an unsupported ten seconds", () => {
|
||||
expect(sourceSkill(490438).effects).toEqual([{
|
||||
kind: "status",
|
||||
status: "stun",
|
||||
durationMs: 6_000,
|
||||
maxTargets: 1,
|
||||
breakOnDamage: true,
|
||||
}]);
|
||||
});
|
||||
|
||||
it("gives Champion nearby and fan spells their source-shaped target areas", () => {
|
||||
const bySpellId = (spellId: number) => ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === spellId)!;
|
||||
expect(bySpellId(498481)).toMatchObject({ target: "self", radius: 10 });
|
||||
expect(bySpellId(498537)).toMatchObject({ target: "self", radius: 7.5 });
|
||||
expect(bySpellId(498721)).toMatchObject({
|
||||
target: "self",
|
||||
radius: 10,
|
||||
targeting: { shape: "circle", radius: 10 },
|
||||
});
|
||||
expect(bySpellId(498534)).toMatchObject({
|
||||
target: "self",
|
||||
radius: 10,
|
||||
targeting: { shape: "cone", radius: 10, attackArcRadians: Math.PI / 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it("restores Rune Pulse Rage on the Champion after its hostile hit", () => {
|
||||
const runePulse = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 498529)!;
|
||||
expect(runePulse.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
recipient: "caster",
|
||||
aura: expect.objectContaining({ modifiers: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "resource", resource: "rage", property: "regeneration" }),
|
||||
]) }),
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
it("puts Scout/Mage Aggro Transfer mana regeneration on the selected ally", () => {
|
||||
const aggroTransfer = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 493255)!;
|
||||
expect(aggroTransfer.target).toBe("friendly");
|
||||
expect(aggroTransfer.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
recipient: "ability-target",
|
||||
aura: expect.objectContaining({ modifiers: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "resource", resource: "mana", property: "regeneration" }),
|
||||
]) }),
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
it("preserves Druid elite healing and chain targeting semantics", () => {
|
||||
const bySpellId = (spellId: number) => ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === spellId)!;
|
||||
const arrows = bySpellId(494021);
|
||||
expect(arrows).toMatchObject({ target: "friendly", targeting: { shape: "circle", radius: 20, maximumTargets: 5 } });
|
||||
expect(arrows.effects.map((effect) => effect.kind)).toEqual(["hot", "hot"]);
|
||||
const arrowHots = arrows.effects.filter((effect): effect is Extract<typeof effect, { kind: "hot" }> => effect.kind === "hot");
|
||||
expect(arrowHots).toHaveLength(2);
|
||||
expect(arrowHots[0]!.basePoints).toBeCloseTo(23.6);
|
||||
expect(arrowHots[1]!.basePoints).toBeCloseTo(18.88);
|
||||
const earthChain = bySpellId(494025);
|
||||
expect(earthChain).toMatchObject({ targeting: { shape: "chain", maximumTargets: 3, jumpRange: 6 } });
|
||||
expect(earthChain.effects.filter((effect) => effect.kind === "damage")).toEqual([
|
||||
expect.objectContaining({ kind: "damage", falloffPerJump: 0.7 }),
|
||||
expect.objectContaining({ kind: "damage", falloffPerJump: 0.7 }),
|
||||
]);
|
||||
const ripple = bySpellId(494323);
|
||||
expect(ripple).toMatchObject({ targeting: { shape: "circle", radius: 6, maximumTargets: 3 } });
|
||||
expect(ripple.effects.filter((effect) => effect.kind === "heal")).toEqual([
|
||||
expect.objectContaining({ falloffPerJump: 0.5 }),
|
||||
expect.objectContaining({ falloffPerJump: 0.5 }),
|
||||
]);
|
||||
expect(bySpellId(493540).targeting?.shape).toBe("line");
|
||||
expect(bySpellId(494217)).toMatchObject({ target: "self", targeting: { shape: "circle", radius: 5 } });
|
||||
expect(bySpellId(494217).effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "dispel", relationship: "friendly", mode: "cleanse" }),
|
||||
]));
|
||||
expect(bySpellId(494027)).toMatchObject({ target: "friendly", targeting: { shape: "circle", radius: 6, maximumTargets: 3 } });
|
||||
expect(bySpellId(494026).targeting).toMatchObject({ shape: "circle", radius: 12 });
|
||||
});
|
||||
|
||||
it("models Champion Overrule as control immunity instead of applying control", () => {
|
||||
const overrule = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 498570)!;
|
||||
expect(overrule.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
recipient: "caster",
|
||||
aura: expect.objectContaining({
|
||||
durationMs: 3_000,
|
||||
controlImmunities: expect.arrayContaining(["stun", "root", "slow"]),
|
||||
}),
|
||||
}),
|
||||
]));
|
||||
expect(overrule.effects.some((effect) => effect.kind === "apply-aura" && effect.control)).toBe(false);
|
||||
});
|
||||
|
||||
it("models Free Will as a 10-second Root and Slow immunity for its friendly target", () => {
|
||||
const freeWill = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 491351)!;
|
||||
expect(freeWill).toMatchObject({ target: "friendly" });
|
||||
expect(freeWill.effects).toEqual([expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
recipient: "ability-target",
|
||||
aura: expect.objectContaining({
|
||||
disposition: "buff",
|
||||
durationMs: 10_000,
|
||||
controlImmunities: ["root", "slow"],
|
||||
}),
|
||||
})]);
|
||||
expect(freeWill.effects.some((effect) => effect.kind === "apply-aura" && effect.control)).toBe(false);
|
||||
});
|
||||
|
||||
it("applies Fire Ward to the Priest's party", () => {
|
||||
expect(named("Fire Ward").effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "apply-aura", scope: "party" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("fans out Warden Crystalline Protection and Acceleration within their source radius", () => {
|
||||
const protection = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 498201)!;
|
||||
const acceleration = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 498202)!;
|
||||
for (const ability of [protection, acceleration]) {
|
||||
expect(ability).toMatchObject({
|
||||
target: "self",
|
||||
radius: 7.5,
|
||||
targeting: { shape: "circle", radius: 7.5 },
|
||||
});
|
||||
expect(ability.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "apply-aura", scope: "party" }),
|
||||
]));
|
||||
}
|
||||
expect(protection.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
scope: "party",
|
||||
aura: expect.objectContaining({ absorbs: expect.arrayContaining([expect.any(Object)]) }),
|
||||
}),
|
||||
]));
|
||||
expect(acceleration.effects.flatMap((effect) => effect.kind === "apply-aura" ? effect.aura.modifiers ?? [] : []))
|
||||
.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "stat", stat: "attack-speed" }),
|
||||
expect.objectContaining({ kind: "stat", stat: "spell-haste" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("applies Coat of Arms defenses to both the caster and selected ally", () => {
|
||||
const coatOfArms = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 494611)!;
|
||||
expect(coatOfArms.target).toBe("friendly");
|
||||
expect(coatOfArms.effects).toHaveLength(2);
|
||||
expect(coatOfArms.effects).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
recipient: "caster",
|
||||
aura: expect.objectContaining({
|
||||
durationMs: 20_000,
|
||||
modifiers: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "stat", stat: "armor", operation: "percent", value: 0.3 }),
|
||||
expect.objectContaining({ kind: "stat", stat: "magical-defense", operation: "percent", value: 0.3 }),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({ kind: "apply-aura", recipient: "ability-target" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains every executable part of multi-effect abilities", () => {
|
||||
const kinds = named("Sneak Attack").effects.map((effect) => effect.kind);
|
||||
expect(kinds).toEqual(["damage", "apply-aura", "dot"]);
|
||||
@@ -170,6 +437,18 @@ describe("executable RuneWaker ability mappings", () => {
|
||||
]));
|
||||
});
|
||||
|
||||
it("marks Briar Shield as the source prerequisite for Thorn Arrow", () => {
|
||||
expect(sourceSkill(493642).effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
aura: expect.objectContaining({ utilityTags: expect.arrayContaining(["rom-briar-shield"]) }),
|
||||
}),
|
||||
]));
|
||||
expect(sourceSkill(494037).effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "damage" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("models the Knight's Holy Seal build-and-spend loop", () => {
|
||||
expect(named("Holy Strike").effects).toEqual(expect.arrayContaining([
|
||||
{ kind: "combo", amount: 1 },
|
||||
|
||||
Reference in new issue
Block a user