Complete RoM pair elite mechanics and creature family rules
This commit is contained in:
1 parent
c8b3a3d95a
commit
0206fda9e2
21 files changed
+1082
-134
No files matched your search
@@ -1,9 +1,8 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { abilitiesForCharacter, learnedAbilityForBuild, type AbilityDefinition } from "./abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { romScoutRogueCastEffects, romScoutRogueProcActionSpec } from "./romScoutRogueOverrides";
|
||||
import { romScoutRogueCastEffects, romScoutRogueProcActionSpec, romScoutRogueWhirlwindBonusEffect } from "./romScoutRogueOverrides";
|
||||
|
||||
const scoutRogue = abilitiesForCharacter("rom-scout", "rom-rogue");
|
||||
const rogueWarrior = abilitiesForCharacter("rom-rogue", "rom-warrior");
|
||||
@@ -122,7 +121,7 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => {
|
||||
expect(new Set(damage).size).toBe(1);
|
||||
});
|
||||
|
||||
it("Master Reflected Shot applies a labeled 20% hybrid damage bonus", () => {
|
||||
it("Master Reflected Shot uses replacement 499535's 0.85 / 0.75 DPS ratio", () => {
|
||||
const ability = skill(scoutRogue, "rom-scout-primary-11");
|
||||
const base = ability.effects.find((effect) => effect.kind === "damage");
|
||||
const mastered = romScoutRogueCastEffects(ability, ability.effects,
|
||||
@@ -131,7 +130,7 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => {
|
||||
expect(base?.kind).toBe("damage");
|
||||
expect(mastered?.kind).toBe("damage");
|
||||
if (base?.kind !== "damage" || mastered?.kind !== "damage") return;
|
||||
expect(mastered.coefficient).toBeCloseTo(base.coefficient * 1.2);
|
||||
expect(mastered.coefficient).toBeCloseTo(base.coefficient * 0.85 / 0.75);
|
||||
expect(mastered.falloffPerJump).toBe(1);
|
||||
});
|
||||
|
||||
@@ -160,12 +159,11 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => {
|
||||
expect(proc?.kind === "apply-aura" ? proc.aura.procs?.[0]?.internalCooldownMs : null).toBe(20_000);
|
||||
});
|
||||
|
||||
it("proc resolver poisons only Reflected or Combo Shot while carrying Poison Powder", () => {
|
||||
it("proc resolver poisons Reflected or Combo Shot with unlimited Poison Powder", () => {
|
||||
for (const sourceId of [490457, 490424]) {
|
||||
expect(romScoutRogueProcActionSpec("rom-poisonous-spit", sourceId, false)).toBeNull();
|
||||
expect(romScoutRogueProcActionSpec("rom-poisonous-spit", sourceId, true)).toMatchObject({
|
||||
kind: "dot", recipient: "target", provenance: "hybrid-estimate",
|
||||
effect: { kind: "dot", ticks: 5, intervalMs: 2_000 },
|
||||
expect(romScoutRogueProcActionSpec("rom-poisonous-spit", sourceId, false)).toMatchObject({
|
||||
kind: "dot", recipient: "target", provenance: "source",
|
||||
effect: { kind: "dot", ticks: 4, intervalMs: 2_000, basePoints: 7.1, pointsPerLevel: 2.1 },
|
||||
});
|
||||
}
|
||||
expect(romScoutRogueProcActionSpec("rom-poisonous-spit", 490423, true)).toBeNull();
|
||||
@@ -175,15 +173,6 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => {
|
||||
|
||||
it("executes the Scout/Rogue poison rider on a successful reflected hit", () => {
|
||||
prime("rom-scout", 60);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
const poison = skill(rogueWarrior, "rom-rogue-primary-6");
|
||||
const state = poison.effects
|
||||
.filter((effect): effect is Extract<AbilityDefinition["effects"][number], { kind: "apply-aura" }> => effect.kind === "apply-aura")
|
||||
.find((effect) => effect.aura.utilityTags?.includes("rom-poisonous") === true);
|
||||
expect(state).toBeDefined();
|
||||
expect(useCombatStore.getState().applyActorAura(
|
||||
PLAYER_AGGRO_ID, PLAYER_AGGRO_ID, poison, state!, 1_000,
|
||||
)).toBe(true);
|
||||
expect(useCombatStore.getState().castAbility("rom-scout-primary-11", undefined, 2_600))
|
||||
.toMatchObject({ ok: true });
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-poisonous-spit"
|
||||
@@ -202,11 +191,112 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => {
|
||||
expect(proc?.kind === "apply-aura" ? proc.aura.procs?.[0]?.chance : null).toBe(0.25);
|
||||
});
|
||||
|
||||
it("keeps absent 499477/499481 replacement-skill procs explicitly unresolved", () => {
|
||||
it("Toxic Splash's 493296 Infection upgrade can return 20 rage on its hit", () => {
|
||||
expect(romScoutRogueProcActionSpec("rom-infection-rage", 490338, false)).toBeNull();
|
||||
expect(romScoutRogueProcActionSpec("rom-infection-rage", 492625, false)).toMatchObject({
|
||||
kind: "resource", recipient: "caster", resource: "rage", amount: 20,
|
||||
provenance: "hybrid-estimate",
|
||||
});
|
||||
const proc = skill(rogueWarrior, "rom-rogue-rom-warrior-elite-60").effects.find((effect) => effect.kind === "apply-aura");
|
||||
expect(proc?.kind === "apply-aura" ? proc.aura.procs?.[0]?.chance : null).toBe(0.25);
|
||||
});
|
||||
|
||||
it("maps 499477/499481 through cast upgrades rather than redundant procs", () => {
|
||||
expect(romScoutRogueProcActionSpec("rom-toxic-splash", 499477, true)).toBeNull();
|
||||
expect(romScoutRogueProcActionSpec("rom-poison-battle-technique", 499481, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("Meridian Attack upgrades Wrist Attack's physical and spell damage penalty", () => {
|
||||
const ability = skill(scoutRogue, "rom-scout-general-6");
|
||||
expect(ability.dbcSpellId).toBe(490451);
|
||||
const upgraded = romScoutRogueCastEffects(ability, ability.effects, ["rom-meridian-attack"], []);
|
||||
const debuff = upgraded.find((effect) => effect.kind === "apply-aura" && effect.recipient !== "caster");
|
||||
expect(debuff?.kind === "apply-aura" ? debuff.aura : null).toMatchObject({
|
||||
durationMs: 10_000,
|
||||
modifiers: expect.arrayContaining([
|
||||
{ kind: "damage", direction: "dealt", school: "physical", operation: "percent", value: -0.3 },
|
||||
{ kind: "damage", direction: "dealt", attackKind: "spell", operation: "percent", value: -0.3 },
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("applies Meridian Attack's ten-second Wrist Attack debuff at level 79", () => {
|
||||
prime("rom-scout", 79);
|
||||
expect(useCombatStore.getState().castAbility("rom-scout-general-6", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
const debuff = useCombatStore.getState().auras.find((aura) => aura.targetId === "target"
|
||||
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "damage"
|
||||
&& modifier.direction === "dealt" && modifier.attackKind === "spell"));
|
||||
expect(debuff?.definition.durationMs).toBe(10_000);
|
||||
});
|
||||
|
||||
it("Toxic Splash upgrades Poison Spray to 493296's hit and 622992's six-second poison", () => {
|
||||
const ability = skill(rogueWarrior, "rom-rogue-rom-warrior-elite-40");
|
||||
const upgraded = romScoutRogueCastEffects(ability, ability.effects, ["rom-toxic-splash"], []);
|
||||
expect(upgraded.filter((effect) => effect.kind === "damage")).toHaveLength(2);
|
||||
expect(upgraded.filter((effect) => effect.kind === "damage")[1]).toMatchObject({
|
||||
kind: "damage", coefficient: 1.11375, coefficientPerLevel: 0.06375,
|
||||
});
|
||||
expect(upgraded.find((effect) => effect.kind === "dot")).toMatchObject({
|
||||
kind: "dot", ticks: 3, intervalMs: 2_000, tag: "rom-622992",
|
||||
basePoints: 16.2, pointsPerLevel: 4.2,
|
||||
});
|
||||
});
|
||||
|
||||
it("spreads Toxic Splash's replacement poison to nearby enemies at level 79", () => {
|
||||
prime("rom-rogue", 79);
|
||||
useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 100 } }));
|
||||
expect(useCombatStore.getState().castAbility("rom-rogue-primary-6", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(1.1, 2_100);
|
||||
const infectionCast = useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-35", undefined, 2_600);
|
||||
expect(infectionCast, infectionCast.reason).toMatchObject({ ok: true });
|
||||
expect(useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-40", undefined, 4_200))
|
||||
.toMatchObject({ ok: true, affectedMobIds: expect.arrayContaining(["target", "nearby"]) });
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-622992"
|
||||
&& effect.targetId === "nearby")).toBe(true);
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "dot"
|
||||
&& effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35" && effect.targetId === "target")).toBe(false);
|
||||
});
|
||||
|
||||
it("Poison Battle Technique upgrades Whirlwind and adds a per-target poisoned hit", () => {
|
||||
const ability = skill(rogueWarrior, "rom-warrior-general-2");
|
||||
const base = ability.effects.find((effect) => effect.kind === "damage");
|
||||
const upgraded = romScoutRogueCastEffects(ability, ability.effects, ["rom-poison-battle-technique"], [])
|
||||
.find((effect) => effect.kind === "damage");
|
||||
expect(base?.kind).toBe("damage");
|
||||
expect(upgraded?.kind).toBe("damage");
|
||||
if (base?.kind !== "damage" || upgraded?.kind !== "damage") return;
|
||||
expect(upgraded.coefficient).toBeCloseTo(base.coefficient * 0.75 / 0.70);
|
||||
expect(romScoutRogueWhirlwindBonusEffect(ability, ["rom-poison-battle-technique"], []))
|
||||
.toBeNull();
|
||||
expect(romScoutRogueWhirlwindBonusEffect(ability, ["rom-poison-battle-technique"], ["rom-622992"]))
|
||||
.toMatchObject({ kind: "damage", coefficient: 0.5, coefficientPerLevel: 0.03 });
|
||||
});
|
||||
|
||||
it("Whirlwind grants its Poison Battle Technique rider only to sprayed targets at level 79", () => {
|
||||
prime("rom-rogue", 79);
|
||||
useCombatStore.getState().registerMob("clean", { name: "clean", maxHealth: 1_000_000 });
|
||||
setMobPosition("clean", [-4, 0, 0]); // Whirlwind range, outside Spray's target-centered radius.
|
||||
useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 100 } }));
|
||||
expect(useCombatStore.getState().castAbility("rom-rogue-primary-6", undefined, 1_000)).toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(1.1, 2_100);
|
||||
expect(useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-35", undefined, 2_600))
|
||||
.toMatchObject({ ok: true });
|
||||
expect(useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-40", undefined, 4_200))
|
||||
.toMatchObject({ ok: true });
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-622992" && effect.targetId === "target"))
|
||||
.toBe(true);
|
||||
expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-622992" && effect.targetId === "clean"))
|
||||
.toBe(false);
|
||||
expect(useCombatStore.getState().castAbility("rom-warrior-general-2", undefined, 5_800))
|
||||
.toMatchObject({ ok: true, affectedMobIds: expect.arrayContaining(["target", "clean"]) });
|
||||
const hits = useCombatStore.getState().combatEvents.filter((event) => event.abilityId === "rom-warrior-general-2"
|
||||
&& event.kind === "damage");
|
||||
expect(hits.filter((event) => event.targetActorId === "target")).toHaveLength(2);
|
||||
expect(hits.filter((event) => event.targetActorId === "clean")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("Rogue Combo Throw has two hits and Poison grants a proc state (490338, 490329)", () => {
|
||||
expect(skill(rogueWarrior, "rom-rogue-primary-15").effects.filter((effect) => effect.kind === "damage")).toHaveLength(2);
|
||||
prime("rom-rogue", 60);
|
||||
|
||||
Reference in new issue
Block a user