forked from phenom/healer-man
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,12 +1,18 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { abilitiesForCharacter, abilityById, learnedAbilityForBuild } from "./abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { applyAura as applyCombatAura } from "./combatAuras";
|
||||
import { abilityDamageSchool, useCombatStore } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
|
||||
import {
|
||||
ROM_MAGE_WARLOCK_PASSIVE_TAGS,
|
||||
ROM_BREATH_ERASE_THREAT_TAG,
|
||||
ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS,
|
||||
romMageWarlockCastEffects,
|
||||
romMageWarlockCriticalEffects,
|
||||
romFireLightningBurstReset,
|
||||
romMageWarlockThreatScale,
|
||||
} from "./romMageWarlockOverrides";
|
||||
|
||||
const ELITES = [
|
||||
@@ -78,6 +84,8 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
}),
|
||||
}),
|
||||
]));
|
||||
expect(elite(30).effects.some((effect) => effect.kind === "apply-aura"
|
||||
&& effect.aura.utilityTags?.includes(ROM_BREATH_ERASE_THREAT_TAG))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses magic schools for Mage and Warlock magic projectiles and dark spells", () => {
|
||||
@@ -150,6 +158,23 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
expect(useCombatStore.getState().mobs["weave-far"]!.health).toBe(100_000);
|
||||
});
|
||||
|
||||
it("caps Surge of Malice at three targets even when four enemies are nearby", () => {
|
||||
initialize(30);
|
||||
for (const [id, x] of [
|
||||
["surge-a", 2], ["surge-b", 3], ["surge-c", 4], ["surge-d", 5], ["surge-far", 20],
|
||||
] as const) {
|
||||
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 });
|
||||
setMobPosition(id, [x, 0, 0]);
|
||||
}
|
||||
useCombatStore.getState().selectMob("surge-a");
|
||||
expect(useCombatStore.getState().castAbility("rom-warlock-general-5", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
const damaged = ["surge-a", "surge-b", "surge-c", "surge-d"]
|
||||
.filter((id) => useCombatStore.getState().mobs[id]!.health < 100_000);
|
||||
expect(damaged).toHaveLength(3);
|
||||
expect(useCombatStore.getState().mobs["surge-far"]!.health).toBe(100_000);
|
||||
});
|
||||
|
||||
it("Static Resonance resets only Electrostatic Charge", () => {
|
||||
initialize(60);
|
||||
useCombatStore.setState({ cooldowns: {
|
||||
@@ -196,6 +221,19 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
expect(useCombatStore.getState().mobs["moving-cast-dummy"]!.health).toBeLessThan(100_000);
|
||||
});
|
||||
|
||||
it("Soul Speed adds its movement and spell speed buffs when Soul Stepping is cast", () => {
|
||||
initialize(60);
|
||||
expect(useCombatStore.getState().castAbility(elite(40).id, undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
const modifiers = useCombatStore.getState().auras
|
||||
.filter((aura) => aura.targetId === PLAYER_AGGRO_ID)
|
||||
.flatMap((aura) => aura.definition.modifiers ?? []);
|
||||
expect(modifiers).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "stat", stat: "movement-speed", value: 0.2 }),
|
||||
expect.objectContaining({ kind: "stat", stat: "spell-haste", value: 20 }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("Fire Lightning Burst hits twice and resets its own cooldown only if the target survives", () => {
|
||||
initialize(60);
|
||||
registerTarget();
|
||||
@@ -206,6 +244,10 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
expect(useCombatStore.getState().mobs["mage-warlock-dummy"]!.health).toBeLessThan(100_000);
|
||||
expect(useCombatStore.getState().cooldowns[elite(50).id] ?? 0).toBeLessThanOrEqual(3_100);
|
||||
expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(90_000);
|
||||
|
||||
expect(useCombatStore.getState().castAbility(elite(50).id, undefined, 4_600)).toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 6_700);
|
||||
expect(useCombatStore.getState().cooldowns[elite(50).id]).toBeGreaterThan(6_700);
|
||||
});
|
||||
|
||||
it("does not apply conditional passive buffs or enemy defense penalties to the caster on login", () => {
|
||||
@@ -217,16 +259,52 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
expect(elite(20).effects.some((effect) => effect.kind === "scripted")).toBe(false);
|
||||
});
|
||||
|
||||
it("converts Soul Pain to fire with an extra hit only while Industry Fire is learned", () => {
|
||||
it("uses Industry Fire's hidden Soul Pain payload during a live cast", () => {
|
||||
initialize(60);
|
||||
registerTarget("industry-fire-dummy");
|
||||
expect(useCombatStore.getState().castAbility("rom-warlock-general-4", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 3_100);
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.mobs["industry-fire-dummy"]!.health).toBeLessThan(100_000);
|
||||
expect(state.effects.some((effect) => effect.tag === "rom-industry-fire"
|
||||
&& effect.targetId === "industry-fire-dummy")).toBe(true);
|
||||
});
|
||||
|
||||
it("triggers Lightning Display and Elements of Pride only after their source spells land", () => {
|
||||
initialize(60);
|
||||
registerTarget("elite-trigger-dummy");
|
||||
expect(useCombatStore.getState().castAbility("rom-mage-primary-52", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 3_100);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
|
||||
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
|
||||
&& modifier.stat === "magical-critical-strike" && modifier.value === 20))).toBe(true);
|
||||
|
||||
expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 4_600))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 7_700);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
|
||||
&& aura.definition.id === "rom-mage-warlock-elements-nature" && aura.stacks === 1)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses hidden skill 498746's exact fire hit and DoT after Industry Fire", () => {
|
||||
const soulPain = abilityById("rom-warlock-general-4")!;
|
||||
const ordinary = romMageWarlockCastEffects(soulPain, soulPain.effects, new Set());
|
||||
const upgraded = romMageWarlockCastEffects(soulPain, soulPain.effects,
|
||||
new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire]));
|
||||
expect(ordinary.some((effect) => effect.kind === "damage")).toBe(false);
|
||||
expect(upgraded).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "dot", school: "fire" }),
|
||||
expect.objectContaining({ kind: "damage", school: "fire" }),
|
||||
]));
|
||||
expect(upgraded).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "damage", school: "fire", coefficient: 0,
|
||||
basePoints: 140, pointsPerLevel: 40, sourceLevel: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dot", school: "fire", coefficient: 0,
|
||||
basePoints: 23.4, pointsPerLevel: 10.4, sourceLevel: 1,
|
||||
ticks: 24, intervalMs: 1_000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("extends Charged, creates conditional Elements stacks, and enhances Soul Stepping", () => {
|
||||
@@ -240,6 +318,20 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-charged")
|
||||
));
|
||||
expect(charged).toMatchObject({ aura: { durationMs: 18_000 } });
|
||||
const lightningBuff = romMageWarlockCastEffects(plasma, plasma.effects, tags).find((effect) => (
|
||||
effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => (
|
||||
modifier.kind === "stat" && modifier.stat === "magical-critical-strike"
|
||||
))
|
||||
));
|
||||
expect(lightningBuff).toMatchObject({
|
||||
aura: {
|
||||
durationMs: 18_000,
|
||||
modifiers: expect.arrayContaining([
|
||||
expect.objectContaining({ stat: "magical-critical-strike", value: 20 }),
|
||||
expect.objectContaining({ kind: "damage", school: "nature", value: 0.01 }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const flame = abilityById("rom-mage-primary-1")!;
|
||||
expect(romMageWarlockCastEffects(flame, flame.effects, tags)).toEqual(expect.arrayContaining([
|
||||
@@ -258,20 +350,138 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => {
|
||||
});
|
||||
|
||||
it("keeps Elemental Extraction's defense debuff on a critical-hit target", () => {
|
||||
const explosion = abilityById("rom-mage-primary-54")!;
|
||||
expect(romMageWarlockCriticalEffects(explosion, new Set())).toEqual([]);
|
||||
expect(romMageWarlockCriticalEffects(explosion,
|
||||
new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura", recipient: "ability-target",
|
||||
aura: expect.objectContaining({
|
||||
disposition: "debuff", durationMs: 10_000,
|
||||
modifiers: expect.arrayContaining([
|
||||
expect.objectContaining({ stat: "armor", value: -0.3 }),
|
||||
expect.objectContaining({ stat: "magical-defense", value: -0.3 }),
|
||||
]),
|
||||
for (const id of ["rom-mage-primary-1", "rom-mage-primary-54"]) {
|
||||
const attack = abilityById(id)!;
|
||||
expect(romMageWarlockCriticalEffects(attack, new Set())).toEqual([]);
|
||||
expect(romMageWarlockCriticalEffects(attack,
|
||||
new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "apply-aura", recipient: "ability-target",
|
||||
aura: expect.objectContaining({
|
||||
disposition: "debuff", durationMs: 10_000,
|
||||
modifiers: expect.arrayContaining([
|
||||
expect.objectContaining({ stat: "armor", value: -0.3 }),
|
||||
expect.objectContaining({ stat: "magical-defense", value: -0.3 }),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("triggers Elemental Extraction's target debuff on a live critical Flame hit", () => {
|
||||
initialize(79);
|
||||
registerTarget("critical-extraction-dummy");
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: applyCombatAura(state.auras, {
|
||||
definition: {
|
||||
id: "mage-warlock-test-guaranteed-crit",
|
||||
name: "Guaranteed critical test trigger",
|
||||
disposition: "buff",
|
||||
durationMs: 10_000,
|
||||
utilityTags: ["next-offensive-critical"],
|
||||
},
|
||||
sourceId: PLAYER_AGGRO_ID,
|
||||
targetId: PLAYER_AGGRO_ID,
|
||||
now: 1_000,
|
||||
}).auras,
|
||||
}));
|
||||
expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 4_100);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === "critical-extraction-dummy"
|
||||
&& aura.definition.id === "rom-mage-warlock-elemental-extraction-debuff")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses hidden 499851's stronger normal Electric Explosion branch at level 70", () => {
|
||||
const explosion = abilityById("rom-mage-primary-54")!;
|
||||
const original = explosion.effects.filter((effect) => effect.kind === "damage");
|
||||
const upgraded = romMageWarlockCastEffects(explosion, explosion.effects,
|
||||
new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))
|
||||
.filter((effect) => effect.kind === "damage");
|
||||
expect(upgraded).toHaveLength(2);
|
||||
expect(upgraded[0]).toEqual(original[0]);
|
||||
expect(upgraded[1]).toMatchObject({
|
||||
school: "nature", coefficient: 0, basePoints: 65, pointsPerLevel: 15, sourceLevel: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("honors Fire Lightning Burst's source six-second reset guard", () => {
|
||||
const burst = elite(50);
|
||||
expect(romFireLightningBurstReset(burst, true, false)).toEqual({
|
||||
reset: true, guardDurationMs: ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS,
|
||||
});
|
||||
expect(romFireLightningBurstReset(burst, true, true)).toEqual({ reset: false, guardDurationMs: 0 });
|
||||
expect(romFireLightningBurstReset(burst, false, false)).toEqual({ reset: false, guardDurationMs: 0 });
|
||||
expect(romFireLightningBurstReset(elite(45), true, false)).toEqual({ reset: false, guardDurationMs: 0 });
|
||||
});
|
||||
|
||||
it("exposes Breath Erase's five-second threat reduction to the runtime", () => {
|
||||
const breath = elite(30).effects.find((effect) => effect.kind === "apply-aura"
|
||||
&& effect.aura.utilityTags?.includes(ROM_BREATH_ERASE_THREAT_TAG));
|
||||
expect(breath).toMatchObject({ aura: { durationMs: 5_000 } });
|
||||
expect(romMageWarlockThreatScale(new Set())).toBe(1);
|
||||
expect(romMageWarlockThreatScale(new Set([ROM_BREATH_ERASE_THREAT_TAG]))).toBe(0.9);
|
||||
});
|
||||
|
||||
it("reduces live spell threat while Breath Erase is active", () => {
|
||||
const castFlame = (useBreath: boolean) => {
|
||||
initialize(60);
|
||||
registerTarget("breath-threat-dummy");
|
||||
if (useBreath) {
|
||||
expect(useCombatStore.getState().castAbility(elite(30).id, undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
}
|
||||
expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 2_600))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 5_700);
|
||||
const target = useCombatStore.getState().mobs["breath-threat-dummy"]!;
|
||||
return {
|
||||
damage: target.maxHealth - target.health,
|
||||
threat: target.threatByActor[PLAYER_AGGRO_ID] ?? 0,
|
||||
};
|
||||
};
|
||||
const baseline = castFlame(false);
|
||||
const reduced = castFlame(true);
|
||||
expect(reduced.damage).toBeGreaterThan(0);
|
||||
expect(reduced.damage).toBe(baseline.damage);
|
||||
expect((reduced.threat - 1) / (baseline.threat - 1)).toBeCloseTo(0.9, 5);
|
||||
});
|
||||
|
||||
it("applies Elemental Extraction's defense reduction to the target's live magic mitigation", () => {
|
||||
const targetId = "extraction-defense-dummy";
|
||||
const castFlame = (useDebuff: boolean) => {
|
||||
initialize(60);
|
||||
useCombatStore.getState().registerMob(targetId, {
|
||||
name: targetId,
|
||||
maxHealth: 100_000,
|
||||
resistances: { fire: 1_000 },
|
||||
});
|
||||
setMobPosition(targetId, [3, 0, 0]);
|
||||
useCombatStore.getState().selectMob(targetId);
|
||||
if (useDebuff) {
|
||||
const rider = romMageWarlockCriticalEffects(abilityById("rom-mage-primary-1")!,
|
||||
new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))[0];
|
||||
expect(rider.kind).toBe("apply-aura");
|
||||
if (rider.kind !== "apply-aura") throw new Error("Elemental Extraction debuff missing");
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: applyCombatAura(state.auras, {
|
||||
definition: rider.aura,
|
||||
sourceId: PLAYER_AGGRO_ID,
|
||||
targetId,
|
||||
now: 1_000,
|
||||
}).auras,
|
||||
}));
|
||||
}
|
||||
expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.getState().tick(0.1, 4_100);
|
||||
return useCombatStore.getState().mobs[targetId]!.maxHealth
|
||||
- useCombatStore.getState().mobs[targetId]!.health;
|
||||
};
|
||||
const baselineDamage = castFlame(false);
|
||||
const reducedDefenseDamage = castFlame(true);
|
||||
expect(baselineDamage).toBeGreaterThan(0);
|
||||
expect(reducedDefenseDamage).toBeGreaterThan(baselineDamage);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user