Validate RoM primary five hybrid stats and pair mechanics

This commit is contained in:
phenom committed 2026-09-23 19:08:44 -04:00
1 parent 8e95480d4f
commit c8b3a3d95a
18 files changed
+2061 -48

No files matched your search

@@ -0,0 +1,353 @@
import { beforeEach, describe, expect, it } from "vitest";
import { abilitiesForCharacter, isAbilityUnlocked, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { useCombatStore } from "./combatStore";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import { usePartyStore } from "./partyStore";
import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES } from "./romAbilityCatalog";
import { romPriestKnightCastEffects } from "./romPriestKnightOverrides";
const PAIRED_SKILLS = [
["rom-priest", "rom-knight", 15, 492355, "Enhanced Grace of Life", "passive"],
["rom-priest", "rom-knight", 20, 492361, "Life Link", "passive"],
["rom-priest", "rom-knight", 25, 491648, "Last Prayer", "elite"],
["rom-priest", "rom-knight", 30, 491366, "Divine Incarnation", "elite"],
["rom-priest", "rom-knight", 35, 492639, "Calm Heart", "elite"],
["rom-priest", "rom-knight", 40, 492934, "Sacrificial Cure", "elite"],
["rom-priest", "rom-knight", 45, 493044, "Holy Power", "elite"],
["rom-priest", "rom-knight", 50, 493271, "Light Fairy", "elite"],
["rom-priest", "rom-knight", 60, 499968, "Splendor Ripple", "passive"],
["rom-priest", "rom-knight", 70, 499957, "Light Guidance", "passive"],
["rom-knight", "rom-scout", 15, 491616, "Arrow of Vengeance", "elite"],
["rom-knight", "rom-scout", 20, 491610, "Enhanced Holy Strike", "passive"],
["rom-knight", "rom-scout", 25, 491613, "Holy Chains", "elite"],
["rom-knight", "rom-scout", 30, 491354, "Heavenly Arrow", "elite"],
["rom-knight", "rom-scout", 35, 490066, "Weaken Weapon Strength", "passive"],
["rom-knight", "rom-scout", 40, 492939, "Sacred Resistance", "elite"],
["rom-knight", "rom-scout", 45, 493046, "Sacred Protection", "elite"],
["rom-knight", "rom-scout", 50, 493273, "Disregard Danger", "elite"],
["rom-knight", "rom-scout", 60, 499926, "Evil Shackles", "passive"],
["rom-knight", "rom-scout", 70, 499923, "Blood Punishment", "passive"],
] as const;
function pairedSkill(sourceId: number): AbilityDefinition {
const found = ROM_ELITE_ABILITIES.find((ability) => ability.dbcSpellId === sourceId);
if (!found) throw new Error(`Missing paired RoM source skill ${sourceId}`);
return found;
}
function executable(effect: AbilityEffect): boolean {
if (effect.kind === "scripted" || effect.kind === "unsupported") return false;
if (effect.kind === "apply-aura") return Boolean(
effect.control || effect.aura.modifiers?.length || effect.aura.absorbs?.length
|| effect.aura.procs?.length || effect.aura.utilityTags?.length
|| effect.aura.controlImmunities?.length || effect.aura.redirectDamagePercent,
);
return true;
}
describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.setState({ playerPosition: [0, 0, 0] });
});
it("has exactly the twenty RuneWaker paired skills, including passive upgrades", () => {
for (const [primary, secondary] of [["rom-priest", "rom-knight"], ["rom-knight", "rom-scout"]] as const) {
const pair = ROM_ELITE_ABILITIES.filter((ability) => ability.classId === primary
&& ability.secondaryClassId === secondary);
expect(pair, `${primary}/${secondary}`).toHaveLength(10);
const kit = abilitiesForCharacter(primary, secondary);
for (const skill of pair) expect(kit).toContain(skill);
}
});
it.each(PAIRED_SKILLS)("maps %s/%s level %i source %i %s (%s) with the right gate", (
primary, secondary, unlockLevel, sourceId, name, group,
) => {
const skill = pairedSkill(sourceId);
expect(skill).toMatchObject({
classId: primary, secondaryClassId: secondary, unlockLevel,
dbcSpellId: sourceId, name, romSkillGroup: group, passive: group === "passive",
});
for (const level of [10, 30, 60, 79]) {
expect(isAbilityUnlocked(skill, level), `${name} at level ${level}`)
.toBe(level >= unlockLevel);
}
});
it.each(PAIRED_SKILLS)("gives %s/%s level %i source %i %s (%s) an executable mechanic", (
_primary, _secondary, _unlockLevel, sourceId, name,
) => {
const skill = pairedSkill(sourceId);
expect(skill.effects.some(executable), `${name} (${sourceId})`).toBe(true);
});
it("retains direct and area Knight/Scout taunts", () => {
expect(pairedSkill(491616).effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "taunt" }),
]));
expect(pairedSkill(491354).effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "taunt", maxTargets: expect.any(Number) }),
]));
});
it("retains Priest/Knight healing costs and offensive/defensive effects", () => {
const sacrifice = pairedSkill(492934);
expect(sacrifice.costs).toEqual(expect.arrayContaining([
expect.objectContaining({ resource: "health", amount: 70, percentage: true }),
]));
expect(sacrifice.effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "heal", percentMaxHealth: expect.any(Number) }),
]));
expect(pairedSkill(491366).effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "apply-aura", aura: expect.objectContaining({
modifiers: expect.arrayContaining([expect.objectContaining({ kind: "healing", direction: "done", value: 0.15 })]),
}) }),
]));
expect(pairedSkill(493271).effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "summon", creatureId: 102108 }),
]));
});
it("retains Knight/Scout damage and Dexterity buff on Sacred Resistance", () => {
const skill = pairedSkill(492939);
expect(skill.effects.some((effect) => effect.kind === "damage" && effect.coefficient > 0)).toBe(true);
expect(skill.effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "dexterity" && modifier.value > 0))).toBe(true);
});
it("makes Calm Heart resist Fear rather than applying an empty buff", () => {
expect(pairedSkill(492639).effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.controlImmunities?.includes("fear"))).toBe(true);
});
it("maps Sacred Protection to a real shield block bonus", () => {
expect(pairedSkill(493046).effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "block-chance" && modifier.value > 0))).toBe(true);
});
it("redirects damage with Disregard Danger", () => {
expect(pairedSkill(493273).effects.some((effect) => effect.kind === "apply-aura"
&& (effect.aura.redirectDamagePercent ?? 0) > 0)).toBe(true);
});
it("applies only the advertised root from Holy Chains", () => {
const controls = pairedSkill(491613).effects.flatMap((effect) => effect.kind === "apply-aura"
&& effect.control ? [effect.control.kind] : []);
expect(controls).toEqual(["root"]);
});
it("increases healing received through Blood Punishment", () => {
expect(pairedSkill(499923).effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.utilityTags?.includes("rom-blood-punishment"))).toBe(true);
expect(pairedSkill(499923).effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "healing"))).toBe(false);
});
it("keeps passive pair upgrades conditional on their parent casts", () => {
for (const [sourceId, tag] of [
[492355, "rom-enhanced-grace-of-life"],
[492361, "rom-life-link"],
[499968, "rom-splendor-ripple"],
[499957, "rom-light-guidance"],
[491610, "rom-enhanced-holy-strike"],
[490066, "rom-weaken-weapon-strength"],
[499926, "rom-evil-shackles"],
[499923, "rom-blood-punishment"],
] as const) {
expect(pairedSkill(sourceId).effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.utilityTags?.includes(tag)), `${sourceId} ${tag}`).toBe(true);
}
expect(pairedSkill(491648).effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.utilityTags?.includes("rom-last-prayer"))).toBe(true);
});
it("applies passive upgrades to Grace, Urgent Heal, Holy Strike, and Heavenly Arrow casts", () => {
const ability = (sourceId: number) => ROM_ALL_ABILITIES.find((row) => row.dbcSpellId === sourceId)!;
const grace = ability(490290);
const enhancedGrace = romPriestKnightCastEffects(grace, grace.effects, ["rom-enhanced-grace-of-life"], 1_000);
expect(enhancedGrace.some((effect) => effect.kind === "apply-aura"
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "resource"
&& modifier.resource === "mana" && modifier.property === "maximum"))).toBe(true);
const urgent = ability(491147);
const linked = romPriestKnightCastEffects(urgent, urgent.effects, ["rom-life-link"], 1_000);
expect(linked.filter((effect) => effect.kind === "heal" && effect.recipient === "caster")).toHaveLength(1);
const holyStrike = ability(490148);
const enhancedStrike = romPriestKnightCastEffects(holyStrike, holyStrike.effects, ["rom-enhanced-holy-strike"], 1_000);
const originalDamage = holyStrike.effects.find((effect) => effect.kind === "damage");
const enhancedDamage = enhancedStrike.find((effect) => effect.kind === "damage");
expect(enhancedDamage?.kind === "damage" && originalDamage?.kind === "damage"
&& enhancedDamage.coefficient > originalDamage.coefficient).toBe(true);
const arrow = ability(491354);
const weakened = romPriestKnightCastEffects(arrow, arrow.effects, ["rom-weaken-weapon-strength"], 1_000);
expect(weakened.some((effect) => effect.kind === "apply-aura"
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "damage"
&& modifier.direction === "dealt" && modifier.school === "physical" && modifier.value < 0))).toBe(true);
});
it("halves Holy Chains duration against ordinary targets after Evil Shackles unlocks", () => {
const chains = pairedSkill(491613);
const rootDuration = (effects: readonly AbilityEffect[]) => effects
.filter((effect): effect is Extract<AbilityEffect, { kind: "apply-aura" }> => effect.kind === "apply-aura")
.find((effect) => effect.control?.kind === "root")?.aura.durationMs;
const ordinary = romPriestKnightCastEffects(chains, chains.effects, ["rom-evil-shackles"], 1_000);
const undead = romPriestKnightCastEffects(chains, chains.effects,
["rom-evil-shackles", "rom-target-undead-demon"], 1_000);
expect(rootDuration(ordinary)).toBe(rootDuration(chains.effects)! / 2);
expect(rootDuration(undead)).toBe(rootDuration(chains.effects));
});
it("heals multiple nearby allies with Priest Group Heal", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer",
});
usePartyStore.getState().initializeParty("priest-group-heal", 30, useCombatStore.getState().health, "healer", 3, "rom");
const members = usePartyStore.getState().members.slice(0, 2);
expect(members).toHaveLength(2);
members.forEach((member, index) => {
usePartyStore.getState().damageMember(member.id, Math.max(100, Math.floor(member.maxHealth * 0.4)));
registerPartyRuntimePosition(member.id, [3 + index * 2, 0, 0]);
});
const injured = members.map((member) => usePartyStore.getState().members.find((entry) => entry.id === member.id)!.health);
expect(useCombatStore.getState().castAbility("rom-priest-primary-8", undefined, 1_000)).toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 4_100);
members.forEach((member, index) => {
const healed = usePartyStore.getState().members.find((entry) => entry.id === member.id)!.health;
expect(healed, `Group Heal recipient ${index + 1}`).toBeGreaterThan(injured[index]!);
});
});
it("preserves Soul Source's 90-source-unit all-teammate heal", () => {
const soulSource = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490288)!;
expect(soulSource.radius).toBe(9);
expect(soulSource.effects.some((effect) => effect.kind === "heal"
&& effect.scope === "party")).toBe(true);
});
it("gives Holy Shield physical immunity to the party", () => {
const holyShield = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490495)!;
expect(holyShield.effects.some((effect) => effect.kind === "apply-aura"
&& effect.scope === "party" && effect.aura.modifiers?.some((modifier) => modifier.kind === "damage"
&& modifier.direction === "taken" && modifier.school === "physical" && modifier.value <= -1))).toBe(true);
});
it("taunts nearby enemies with Knight/Scout Heavenly Arrow", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 30, role: "tank",
});
for (const [id, x] of [["arrow-a", 3], ["arrow-b", 5]] as const) {
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 10_000 });
setMobPosition(id, [x, 0, 0]);
}
useCombatStore.getState().selectMob("arrow-a");
expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-30", undefined, 1_000))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().mobs["arrow-a"]!.forcedTarget?.actorId).toBe(PLAYER_AGGRO_ID);
expect(useCombatStore.getState().mobs["arrow-b"]!.forcedTarget?.actorId).toBe(PLAYER_AGGRO_ID);
});
it("damages multiple nearby enemies with Knight Whirlwind Shield", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 30, role: "tank",
});
for (const [id, x] of [["shield-a", 3], ["shield-b", 4]] as const) {
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 10_000 });
setMobPosition(id, [x, 0, 0]);
}
useCombatStore.getState().selectMob("shield-a");
expect(useCombatStore.getState().castAbility("rom-knight-primary-3", undefined, 1_000))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().mobs["shield-a"]!.health).toBeLessThan(10_000);
expect(useCombatStore.getState().mobs["shield-b"]!.health).toBeLessThan(10_000);
});
it("pays Sacrificial Cure's HP cost while healing an ally", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer",
});
usePartyStore.getState().initializeParty("priest-sacrifice", 60, useCombatStore.getState().health, "healer", 2, "rom");
const member = usePartyStore.getState().members[0]!;
usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.6));
registerPartyRuntimePosition(member.id, [3, 0, 0]);
usePartyStore.getState().selectMember(member.id);
const casterHealth = useCombatStore.getState().health;
const allyHealth = usePartyStore.getState().members[0]!.health;
expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-40", undefined, 1_000))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().health).toBeLessThan(casterHealth);
expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(allyHealth);
});
it("Life Link lets Urgent Heal also heal the Priest", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer",
});
usePartyStore.getState().initializeParty("priest-life-link", 30, useCombatStore.getState().health, "healer", 2, "rom");
const member = usePartyStore.getState().members[0]!;
useCombatStore.getState().damagePlayer(Math.floor(useCombatStore.getState().maxHealth * 0.5));
usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.5));
registerPartyRuntimePosition(member.id, [3, 0, 0]);
usePartyStore.getState().selectMember(member.id);
const casterHealth = useCombatStore.getState().health;
const allyHealth = usePartyStore.getState().members[0]!.health;
expect(useCombatStore.getState().castAbility("rom-priest-general-0", undefined, 1_000))
.toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 2_100);
expect(useCombatStore.getState().health).toBeGreaterThan(casterHealth);
expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(allyHealth);
});
it("Last Prayer shortens only the next cast to one second", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer",
});
usePartyStore.getState().initializeParty("priest-last-prayer", 30, useCombatStore.getState().health, "healer", 2, "rom");
const member = usePartyStore.getState().members[0]!;
usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.5));
registerPartyRuntimePosition(member.id, [3, 0, 0]);
usePartyStore.getState().selectMember(member.id);
expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-25", undefined, 1_000))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().castAbility("rom-priest-primary-2", undefined, 2_600))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeLessThanOrEqual(1_000);
useCombatStore.getState().tick(0.1, 3_700);
expect(useCombatStore.getState().castAbility("rom-priest-primary-2", undefined, 4_300))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeGreaterThan(1_000);
});
it("summons the Priest/Knight Light Fairy companion", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer",
});
expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-50", undefined, 1_000))
.toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 7_100);
expect(useCombatStore.getState().summons.some((summon) => summon.creatureId === 102108)).toBe(true);
useCombatStore.getState().tick(1, 8_100);
expect(useCombatStore.getState().summons.some((summon) => summon.creatureId === 102108)).toBe(true);
});
it("places Disregard Danger's redirect on a teammate and Parry on the Knight", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 60, role: "tank",
});
usePartyStore.getState().initializeParty("knight-redirect", 60, useCombatStore.getState().health, "tank", 2, "rom");
const member = usePartyStore.getState().members[0]!;
registerPartyRuntimePosition(member.id, [3, 0, 0]);
usePartyStore.getState().selectMember(member.id);
expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-50", undefined, 1_000))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id
&& (aura.definition.redirectDamagePercent ?? 0) > 0)).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "parry"))).toBe(true);
});
});