701 lines
33 KiB
TypeScript
701 lines
33 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { applyAura, expireAuras, resolveDamageValue } from "./combatAuras";
|
|
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
|
import { clearMobRuntimeRegistry, getMobPosition, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { PLAYER_AURA_ENTITY_ID, stableCombatUnit, useCombatStore } from "./combatStore";
|
|
import { romEffectsForCast, type RomCastMechanics } from "./romCombatMechanics";
|
|
import {
|
|
ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID,
|
|
ROM_CHAMPION_SOUL_FORGE_PROFILE,
|
|
ROM_CHAMPION_WAY_OF_FRENZY_PROC_ACTION_ID,
|
|
ROM_CHAMPION_WAY_OF_FRENZY_PROFILE,
|
|
romChampionCastEffects,
|
|
} from "./romChampionOverrides";
|
|
import {
|
|
ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID,
|
|
ROM_CHAMPION_FORGE_RANK_TWO_AURA_ID,
|
|
} from "./romChampionForgeRuntime";
|
|
|
|
const empty: RomCastMechanics = {
|
|
sealStacks: 0,
|
|
bleedCount: 0,
|
|
charged: false,
|
|
mainHandDagger: false,
|
|
casterMaxHealth: 2_000,
|
|
targetHealthPercent: 1,
|
|
};
|
|
|
|
function sourceAbility(spellId: number) {
|
|
const ability = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === spellId);
|
|
if (!ability) throw new Error(`Missing RoM source ability ${spellId}`);
|
|
return ability;
|
|
}
|
|
|
|
function castEffects(spellId: number, context: Partial<RomCastMechanics> = {}) {
|
|
return romEffectsForCast(sourceAbility(spellId), { ...empty, ...context });
|
|
}
|
|
|
|
describe("RoM Champion source-specific behavior", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
useCombatStore.setState({ playerPosition: [0, 0, 0] });
|
|
});
|
|
|
|
|
|
it("ends Rune Overload when its Rage drain reaches zero", () => {
|
|
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 40 });
|
|
useCombatStore.setState((state) => ({
|
|
resource: 30,
|
|
resourcePools: { ...state.resourcePools, rage: 30 },
|
|
}));
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-champion-primary-17", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().auras.some((aura) => (
|
|
aura.definition.utilityTags?.includes("rom-champion-rune-overload")
|
|
))).toBe(true);
|
|
|
|
expect(useCombatStore.getState().resourcePools.rage).toBe(15);
|
|
useCombatStore.getState().tick(1, 2_000);
|
|
|
|
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
|
|
expect(useCombatStore.getState().auras.some((aura) => (
|
|
aura.definition.utilityTags?.includes("rom-champion-rune-overload")
|
|
))).toBe(false);
|
|
});
|
|
|
|
it("applies Fearless Blow's bonus only below 30% and gives its Rage rider to the caster", () => {
|
|
const atThreshold = castEffects(498531, { targetHealthPercent: 0.3 });
|
|
const belowThreshold = castEffects(498531, { targetHealthPercent: 0.299 });
|
|
const baseDamage = atThreshold.find((effect) => effect.kind === "damage");
|
|
const boostedDamage = belowThreshold.find((effect) => effect.kind === "damage");
|
|
expect(baseDamage?.kind).toBe("damage");
|
|
expect(boostedDamage?.kind).toBe("damage");
|
|
if (baseDamage?.kind === "damage" && boostedDamage?.kind === "damage") {
|
|
expect(boostedDamage.coefficient).toBeCloseTo(baseDamage.coefficient * 1.3);
|
|
expect(boostedDamage.basePoints ?? 0).toBeCloseTo((baseDamage.basePoints ?? 0) * 1.3);
|
|
}
|
|
expect(atThreshold.some((effect) => effect.kind === "apply-aura" && effect.aura.id.includes("621179"))).toBe(false);
|
|
const rage = belowThreshold.find((effect) => effect.kind === "apply-aura" && effect.aura.id.includes("621183"));
|
|
expect(rage?.kind).toBe("apply-aura");
|
|
if (rage?.kind === "apply-aura") expect(rage.recipient).toBe("caster");
|
|
});
|
|
|
|
it("adds Rune Draw's source-described pull and moves the selected target toward the Champion", () => {
|
|
const runeDraw = sourceAbility(498564);
|
|
expect(runeDraw.effects.filter((effect) => effect.kind === "pull")).toHaveLength(1);
|
|
expect(runeDraw.effects.some((effect) => effect.kind === "interrupt")).toBe(true);
|
|
expect(runeDraw.effects.some((effect) => effect.kind === "taunt")).toBe(true);
|
|
|
|
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 20 });
|
|
useCombatStore.getState().registerMob("champion-rune-draw-target", {
|
|
name: "Rune Draw Target",
|
|
maxHealth: 100_000,
|
|
});
|
|
setMobPosition("champion-rune-draw-target", [8, 0, 0]);
|
|
useCombatStore.getState().selectMob("champion-rune-draw-target");
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-champion-primary-1", undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(getMobPosition("champion-rune-draw-target")?.[0]).toBeCloseTo(1.5);
|
|
});
|
|
|
|
it("rejects Indomitable Spirit at 65% health and permits it below the threshold", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion",
|
|
secondaryClassId: "rom-warlock",
|
|
raceId: "rom-dwarf",
|
|
level: 70,
|
|
});
|
|
const maxHealth = useCombatStore.getState().maxHealth;
|
|
useCombatStore.setState((state) => ({
|
|
health: Math.ceil(maxHealth * 0.65),
|
|
resource: 20,
|
|
resourcePools: { ...state.resourcePools, rage: 20 },
|
|
}));
|
|
expect(useCombatStore.getState().castAbility("rom-champion-rom-warlock-elite-35", undefined, 1_000))
|
|
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
|
|
|
useCombatStore.setState((state) => ({
|
|
health: Math.floor(maxHealth * 0.65) - 1,
|
|
resource: 20,
|
|
resourcePools: { ...state.resourcePools, rage: 20 },
|
|
}));
|
|
expect(useCombatStore.getState().castAbility("rom-champion-rom-warlock-elite-35", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
});
|
|
|
|
it("activates Forge for five minutes and grants one stack and Rage on a normal hit", () => {
|
|
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 20 });
|
|
useCombatStore.getState().registerMob("champion-forge-target", { name: "Forge Target", maxHealth: 100_000 });
|
|
setMobPosition("champion-forge-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("champion-forge-target");
|
|
|
|
const forge = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 498523)!;
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
const activeForge = useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-champion-forge-active");
|
|
expect(activeForge?.expiresAt).toBe(301_000);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-stack")).toBe(false);
|
|
|
|
useCombatStore.setState({ autoAttackTargetId: "champion-forge-target", nextAutoAttackAt: 1_000 });
|
|
useCombatStore.getState().tick(0.1, 1_000);
|
|
const forgeStack = useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-champion-forge-stack");
|
|
expect(forgeStack?.stacks).toBe(1);
|
|
expect(forgeStack?.expiresAt).toBe(9_000);
|
|
expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThanOrEqual(10);
|
|
});
|
|
|
|
it("keeps rank 1 at level 21 and unlocks the rank 2 Forge source aura at level 22", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 21,
|
|
});
|
|
const forge = sourceAbility(498523);
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-active"))
|
|
.toBe(true);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === ROM_CHAMPION_FORGE_RANK_TWO_AURA_ID))
|
|
.toBe(false);
|
|
|
|
useCombatStore.setState({ level: 22 });
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 4_000)).toMatchObject({ ok: true });
|
|
const rankTwo = useCombatStore.getState().auras.find((aura) => (
|
|
aura.definition.id === ROM_CHAMPION_FORGE_RANK_TWO_AURA_ID
|
|
));
|
|
expect(rankTwo?.expiresAt).toBe(304_000);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-active"))
|
|
.toBe(false);
|
|
});
|
|
|
|
it("grants Chain Drive on an eligible rank 2 auto attack and resets only learned owner cooldowns", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
|
});
|
|
useCombatStore.getState().registerMob("champion-chain-drive-target", {
|
|
name: "Chain Drive Target", maxHealth: 100_000,
|
|
});
|
|
const forge = sourceAbility(498523);
|
|
const runePulse = sourceAbility(498529);
|
|
const shadowPulse = sourceAbility(498726);
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
useCombatStore.setState((state) => ({
|
|
resource: 0,
|
|
resourcePools: { ...state.resourcePools, rage: 0 },
|
|
cooldowns: { ...state.cooldowns, [runePulse.id]: 90_000, [shadowPulse.id]: 90_000 },
|
|
}));
|
|
|
|
const hitSeed = Array.from({ length: 100 }, (_, index) => `champion-chain-drive-${index}`)
|
|
.find((seed) => stableCombatUnit(`${seed}:498590:0`) < 0.3);
|
|
expect(hitSeed).toBeDefined();
|
|
const hit = useCombatStore.getState().damageMob(
|
|
"champion-chain-drive-target",
|
|
100,
|
|
2_000,
|
|
undefined,
|
|
"physical",
|
|
{
|
|
attackKind: "melee",
|
|
seed: hitSeed!,
|
|
abilityId: "party-auto-attack",
|
|
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
|
},
|
|
);
|
|
expect(hit.amount).toBeGreaterThan(0);
|
|
expect(useCombatStore.getState().auras.some((aura) => (
|
|
aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID
|
|
&& aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.expiresAt === 10_000
|
|
))).toBe(true);
|
|
expect(useCombatStore.getState().cooldowns[runePulse.id]).toBe(0);
|
|
expect(useCombatStore.getState().cooldowns[shadowPulse.id]).toBe(0);
|
|
expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThanOrEqual(10);
|
|
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-champion-forge-stack")?.stacks)
|
|
.toBe(1);
|
|
});
|
|
|
|
it("keeps the rank 2 stack and Rage when Chain Drive's fallback roll fails", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
|
});
|
|
useCombatStore.getState().registerMob("champion-chain-drive-failed-target", {
|
|
name: "Chain Drive Failed Target", maxHealth: 100_000,
|
|
});
|
|
const forge = sourceAbility(498523);
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 0 }, resource: 0 }));
|
|
|
|
const failedSeed = Array.from({ length: 100 }, (_, index) => `chain-drive-failed-${index}`)
|
|
.find((seed) => stableCombatUnit(`${seed}:498590:0`) >= 0.8
|
|
&& stableCombatUnit(`${seed}:498590:1`) >= 0.3)!;
|
|
const hit = useCombatStore.getState().damageMob(
|
|
"champion-chain-drive-failed-target", 100, 2_000, undefined, "physical", {
|
|
attackKind: "melee",
|
|
seed: failedSeed,
|
|
abilityId: "party-auto-attack",
|
|
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
|
},
|
|
);
|
|
|
|
expect(hit.amount).toBeGreaterThan(0);
|
|
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-champion-forge-stack")?.stacks)
|
|
.toBe(1);
|
|
expect(useCombatStore.getState().auras.some((aura) => (
|
|
aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID
|
|
))).toBe(false);
|
|
expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThanOrEqual(10);
|
|
});
|
|
|
|
it("does not proc rank 2 Forge from zero or fully absorbed physical packets", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
|
});
|
|
useCombatStore.getState().registerMob("champion-forge-zero-target", {
|
|
name: "Zero Packet Target", maxHealth: 100_000,
|
|
});
|
|
useCombatStore.getState().registerMob("champion-forge-absorbed-target", {
|
|
name: "Absorbed Packet Target", maxHealth: 100_000,
|
|
});
|
|
const forge = sourceAbility(498523);
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
|
|
const zeroHit = useCombatStore.getState().damageMob(
|
|
"champion-forge-zero-target", 0, 2_000, undefined, "physical", {
|
|
attackKind: "melee",
|
|
seed: "champion-forge-zero-hit",
|
|
abilityId: "party-auto-attack",
|
|
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
|
},
|
|
);
|
|
expect(zeroHit.amount).toBe(0);
|
|
|
|
const shieldedAuras = applyAura(useCombatStore.getState().auras, {
|
|
definition: {
|
|
id: "test:champion-forge-physical-absorb",
|
|
name: "Physical absorb fixture",
|
|
disposition: "buff",
|
|
durationMs: null,
|
|
absorbs: [{ id: "test:champion-forge-physical-absorb-pool", amount: 1_000_000, school: "physical" }],
|
|
},
|
|
sourceId: "champion-forge-absorbed-target",
|
|
targetId: "champion-forge-absorbed-target",
|
|
now: 3_000,
|
|
}).auras;
|
|
useCombatStore.setState({ auras: shieldedAuras });
|
|
const absorbedHit = useCombatStore.getState().damageMob(
|
|
"champion-forge-absorbed-target", 100, 3_000, undefined, "physical", {
|
|
attackKind: "melee",
|
|
seed: "champion-forge-absorbed-hit",
|
|
abilityId: "party-auto-attack",
|
|
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
|
},
|
|
);
|
|
|
|
expect(absorbedHit.amount).toBe(0);
|
|
const absorbedEvent = useCombatStore.getState().combatEvents.filter((event) => (
|
|
event.kind === "damage" && event.targetActorId === "champion-forge-absorbed-target"
|
|
)).at(-1);
|
|
expect(absorbedEvent?.absorbed).toBeGreaterThan(0);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-stack"))
|
|
.toBe(false);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID))
|
|
.toBe(false);
|
|
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
|
|
});
|
|
|
|
it("does not proc rank 2 Forge from a positive melee spell packet", () => {
|
|
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 22 });
|
|
useCombatStore.getState().registerMob("champion-forge-melee-spell-target", {
|
|
name: "Melee Spell Target", maxHealth: 100_000,
|
|
});
|
|
expect(useCombatStore.getState().castAbility(sourceAbility(498523).id, undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
useCombatStore.setState((state) => ({ resource: 0, resourcePools: { ...state.resourcePools, rage: 0 } }));
|
|
|
|
useCombatStore.getState().damageMob(
|
|
"champion-forge-melee-spell-target",
|
|
100,
|
|
2_000,
|
|
undefined,
|
|
"physical",
|
|
{
|
|
attackKind: "melee",
|
|
seed: "champion-direct-melee-spell",
|
|
abilityId: sourceAbility(498529).id,
|
|
rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
|
},
|
|
);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-stack"))
|
|
.toBe(false);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID))
|
|
.toBe(false);
|
|
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
|
|
});
|
|
|
|
it("restores Rune Pulse's source-defined Rage only when its damage lands", () => {
|
|
const runePulse = sourceAbility(498529);
|
|
expect(runePulse.description).toContain("When it hits, it will restore");
|
|
|
|
const effects = castEffects(498529);
|
|
const hit = effects.find((effect) => effect.kind === "damage");
|
|
expect(hit?.kind).toBe("damage");
|
|
if (hit?.kind === "damage") expect(hit.onDamageResource).toEqual({ resource: "rage", amount: 5 });
|
|
expect(effects.some((effect) => effect.kind === "apply-aura" && effect.aura.id.includes("-622327-")))
|
|
.toBe(false);
|
|
expect(effects.some((effect) => effect.kind === "apply-aura" && effect.aura.id === "rom-aura-498604-621253-bonus"))
|
|
.toBe(false);
|
|
expect(castEffects(498726)
|
|
.some((effect) => effect.kind === "apply-aura" && effect.aura.id === "rom-aura-498604-621253-bonus"))
|
|
.toBe(false);
|
|
});
|
|
|
|
it("grants Rune Pulse's one-second bonus at cast start and expires it before the two-second impact", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
|
});
|
|
useCombatStore.getState().registerMob("champion-rune-pulse-target", {
|
|
name: "Rune Pulse Target", maxHealth: 100_000,
|
|
});
|
|
setMobPosition("champion-rune-pulse-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("champion-rune-pulse-target");
|
|
const forge = sourceAbility(498523);
|
|
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
useCombatStore.setState((state) => ({ cooldowns: { ...state.cooldowns, [forge.id]: 0 } }));
|
|
const hitSeed = Array.from({ length: 100 }, (_, index) => `rune-pulse-window-${index}`)
|
|
.find((seed) => stableCombatUnit(`${seed}:498590:0`) < 0.3)!;
|
|
useCombatStore.getState().damageMob(
|
|
"champion-rune-pulse-target", 100, 3_000, undefined, "physical", {
|
|
attackKind: "melee",
|
|
seed: hitSeed,
|
|
abilityId: "party-auto-attack",
|
|
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
|
},
|
|
);
|
|
expect(useCombatStore.getState().auras.some((aura) => (
|
|
aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID
|
|
&& aura.expiresAt === 11_000
|
|
))).toBe(true);
|
|
|
|
const runePulse = sourceAbility(498529);
|
|
expect(runePulse.castTimeMs).toBe(2_000);
|
|
expect(useCombatStore.getState().castAbility(runePulse.id, undefined, 3_100)).toMatchObject({ ok: true });
|
|
const startedAura = useCombatStore.getState().auras.find((aura) => (
|
|
aura.definition.id === "rom-aura-498604-621253-bonus"
|
|
));
|
|
const runePulseBonuses = useCombatStore.getState().auras.filter((aura) => (
|
|
aura.definition.id === "rom-aura-498604-621253-bonus"
|
|
));
|
|
expect(startedAura?.appliedAt).toBe(3_100);
|
|
expect(startedAura?.expiresAt).toBe(4_100);
|
|
expect(runePulseBonuses).toHaveLength(1);
|
|
const activeAurasDuringBonus = expireAuras(useCombatStore.getState().auras, 3_500).active;
|
|
const activeAurasWithoutBonus = activeAurasDuringBonus.filter((aura) => (
|
|
aura.definition.id !== "rom-aura-498604-621253-bonus"
|
|
));
|
|
const outgoingDamageWithBonus = resolveDamageValue(100, activeAurasDuringBonus, {
|
|
direction: "dealt", school: "physical", attackKind: "melee",
|
|
});
|
|
const outgoingDamageWithoutBonus = resolveDamageValue(100, activeAurasWithoutBonus, {
|
|
direction: "dealt", school: "physical", attackKind: "melee",
|
|
});
|
|
expect(outgoingDamageWithBonus - outgoingDamageWithoutBonus).toBeCloseTo(20, 10);
|
|
|
|
useCombatStore.getState().tick(2, 5_100);
|
|
const aurasAtImpact = expireAuras(useCombatStore.getState().auras, 5_100).active;
|
|
const runePulseBonusesAtImpact = aurasAtImpact.filter((aura) => (
|
|
aura.definition.id === "rom-aura-498604-621253-bonus"
|
|
));
|
|
const outgoingDamageAtImpact = resolveDamageValue(100, aurasAtImpact, {
|
|
direction: "dealt", school: "physical", attackKind: "melee",
|
|
});
|
|
expect(outgoingDamageAtImpact).toBe(resolveDamageValue(100, aurasAtImpact.filter((aura) => (
|
|
aura.definition.id !== "rom-aura-498604-621253-bonus"
|
|
)), { direction: "dealt", school: "physical", attackKind: "melee" }));
|
|
expect(runePulseBonusesAtImpact).toHaveLength(0);
|
|
});
|
|
|
|
it("routes Rune Siphon's positive source component to the caster as healing", () => {
|
|
const runeSiphon = sourceAbility(498595);
|
|
expect(runeSiphon.secondaryClassId).toBe("rom-warlock");
|
|
expect(runeSiphon.description).toContain("restores (DMG8) HP to the caster");
|
|
|
|
const effects = castEffects(498595);
|
|
expect(effects.filter((effect) => effect.kind === "damage")).toHaveLength(2);
|
|
expect(effects.filter((effect) => effect.kind === "heal")).toEqual([
|
|
expect.objectContaining({ kind: "heal", recipient: "caster", basePoints: 24, pointsPerLevel: 4 }),
|
|
]);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion", secondaryClassId: "rom-warlock", raceId: "rom-dwarf", level: 70,
|
|
});
|
|
useCombatStore.getState().registerMob("champion-rune-siphon-target", {
|
|
name: "Rune Siphon Target",
|
|
maxHealth: 100_000,
|
|
});
|
|
setMobPosition("champion-rune-siphon-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("champion-rune-siphon-target");
|
|
useCombatStore.getState().damagePlayer(Math.floor(useCombatStore.getState().maxHealth / 2));
|
|
useCombatStore.setState((state) => ({
|
|
resource: 100,
|
|
resourcePools: { ...state.resourcePools, rage: 100, focus: 100 },
|
|
}));
|
|
const healthBeforeCast = useCombatStore.getState().health;
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-champion-rom-warlock-elite-20", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().health).toBeGreaterThan(healthBeforeCast);
|
|
});
|
|
|
|
it("arms Soul Forge Mystery's source-backed dark-magic bonus on critical attacks", () => {
|
|
const soulForge = sourceAbility(498652);
|
|
expect(soulForge.secondaryClassId).toBe("rom-warlock");
|
|
expect(soulForge.description).toContain("If critical attack occurs while in effect");
|
|
|
|
const localCritAuras = soulForge.effects.filter((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.criticalAuraComponentId}-`));
|
|
const localDarkAura = soulForge.effects.find((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.darkPowerComponentId}-`));
|
|
expect(localCritAuras.length).toBeGreaterThan(0);
|
|
expect(localCritAuras.every((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.durationMs === 300_000)).toBe(true);
|
|
expect(localDarkAura).toMatchObject({
|
|
kind: "apply-aura",
|
|
aura: {
|
|
durationMs: 10_000,
|
|
modifiers: [expect.objectContaining({ kind: "damage", direction: "dealt", school: "shadow", value: 0.1 })],
|
|
},
|
|
});
|
|
|
|
const effects = castEffects(498652);
|
|
const criticalAura = effects.find((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.criticalAuraComponentId}-`));
|
|
expect(criticalAura?.kind).toBe("apply-aura");
|
|
if (criticalAura?.kind === "apply-aura") {
|
|
expect(criticalAura.aura.procs).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
triggers: "crit",
|
|
chance: 1,
|
|
action: expect.objectContaining({
|
|
kind: "custom",
|
|
id: ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID,
|
|
data: expect.objectContaining({
|
|
sourceSpellId: 498652,
|
|
sourceComponentId: 621341,
|
|
durationMs: 10_000,
|
|
disposition: "buff",
|
|
maxStacks: 1,
|
|
stackBehavior: "refresh",
|
|
target: "aura-source",
|
|
modifierSchool: "shadow",
|
|
modifierValue: 0.1,
|
|
}),
|
|
}),
|
|
}),
|
|
]));
|
|
}
|
|
expect(effects.some((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.id.includes(`-${ROM_CHAMPION_SOUL_FORGE_PROFILE.darkPowerComponentId}-`))).toBe(false);
|
|
});
|
|
|
|
it("grants Soul Forge Mystery's shadow bonus after a critical hit", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion",
|
|
secondaryClassId: "rom-warlock",
|
|
raceId: "rom-dwarf",
|
|
level: 80,
|
|
xp: 0,
|
|
});
|
|
const soulForge = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 498652)!;
|
|
expect(useCombatStore.getState().castAbility(soulForge.id, undefined, 1_000).ok).toBe(true);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-soul-forge-dark-magic"))
|
|
.toBe(false);
|
|
|
|
useCombatStore.getState().registerMob("soul-forge-crit-target", { name: "Target", maxHealth: 100_000, xpReward: 0 });
|
|
useCombatStore.getState().damageMob(
|
|
"soul-forge-crit-target",
|
|
100,
|
|
2_000,
|
|
undefined,
|
|
"shadow",
|
|
{
|
|
attackKind: "spell",
|
|
seed: "soul-forge-guaranteed-critical",
|
|
abilityId: soulForge.id,
|
|
criticalChanceBonusPercent: 100,
|
|
rules: { canMiss: false, canCrit: true, canDodge: false, canParry: false, canBlock: false },
|
|
},
|
|
);
|
|
|
|
expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
sourceId: PLAYER_AURA_ENTITY_ID,
|
|
targetId: PLAYER_AURA_ENTITY_ID,
|
|
definition: expect.objectContaining({
|
|
id: "rom-champion-soul-forge-dark-magic",
|
|
durationMs: ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicAuraDurationMs,
|
|
modifiers: [expect.objectContaining({
|
|
kind: "damage",
|
|
direction: "dealt",
|
|
school: "shadow",
|
|
operation: "percent",
|
|
value: ROM_CHAMPION_SOUL_FORGE_PROFILE.darkMagicBonusPercent,
|
|
})],
|
|
}),
|
|
}),
|
|
]));
|
|
});
|
|
|
|
it("arms Way of Frenzy only on Divine Vengeance and caps its local 2% Stamina stacks at five", () => {
|
|
const wayOfFrenzy = sourceAbility(ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId);
|
|
const divineVengeance = sourceAbility(ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerSpellId);
|
|
expect(wayOfFrenzy).toMatchObject({ classId: "rom-champion", secondaryClassId: "rom-priest", passive: true });
|
|
expect(divineVengeance.id).toBe(ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerAbilityId);
|
|
expect(wayOfFrenzy.effects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: expect.objectContaining({
|
|
id: "rom-champion-way-of-frenzy-passive",
|
|
durationMs: null,
|
|
procs: [expect.objectContaining({
|
|
triggers: ["hit", "crit"],
|
|
abilityIds: [divineVengeance.id],
|
|
chance: 1,
|
|
action: expect.objectContaining({
|
|
kind: "custom",
|
|
id: ROM_CHAMPION_WAY_OF_FRENZY_PROC_ACTION_ID,
|
|
data: expect.objectContaining({
|
|
sourceSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceSpellId,
|
|
sourceComponentId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.sourceComponentId,
|
|
triggerSpellId: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.triggerSpellId,
|
|
durationMs: 60_000,
|
|
maxStacks: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.maxStacks,
|
|
staminaPercentPerStack: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.staminaPercentPerStack,
|
|
}),
|
|
}),
|
|
})],
|
|
}),
|
|
}),
|
|
]));
|
|
expect(wayOfFrenzy.effects.some((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "stamina")))
|
|
.toBe(false);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion",
|
|
secondaryClassId: "rom-priest",
|
|
raceId: "rom-dwarf",
|
|
level: 70,
|
|
});
|
|
useCombatStore.getState().registerMob("way-of-frenzy-target", { name: "Target", maxHealth: 100_000, xpReward: 0 });
|
|
const stackAuraId = "rom-champion-way-of-frenzy-stacks";
|
|
const stackAura = () => useCombatStore.getState().auras.find((aura) => aura.definition.id === stackAuraId);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-way-of-frenzy-passive"))
|
|
.toBe(true);
|
|
expect(stackAura()).toBeUndefined();
|
|
|
|
const unrelatedAbility = sourceAbility(498528);
|
|
useCombatStore.getState().damageMob("way-of-frenzy-target", 100, 2_000, undefined, "physical", {
|
|
attackKind: "melee",
|
|
seed: "way-of-frenzy-unrelated-hit",
|
|
abilityId: unrelatedAbility.id,
|
|
rules: { canMiss: false, canCrit: false, canDodge: false, canParry: false, canBlock: false },
|
|
});
|
|
expect(stackAura()).toBeUndefined();
|
|
|
|
for (let hit = 0; hit < 6; hit += 1) {
|
|
useCombatStore.getState().damageMob("way-of-frenzy-target", 100, 3_000 + hit * 1_000, undefined, "physical", {
|
|
attackKind: "melee",
|
|
seed: `way-of-frenzy-divine-vengeance-${hit}`,
|
|
abilityId: divineVengeance.id,
|
|
rules: { canMiss: false, canCrit: false, canDodge: false, canParry: false, canBlock: false },
|
|
});
|
|
}
|
|
|
|
expect(stackAura()).toMatchObject({
|
|
stacks: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.maxStacks,
|
|
expiresAt: 68_000,
|
|
definition: {
|
|
durationMs: 60_000,
|
|
maxStacks: 5,
|
|
stackBehavior: "add",
|
|
modifiers: [{
|
|
kind: "stat",
|
|
stat: "stamina",
|
|
operation: "percent",
|
|
value: ROM_CHAMPION_WAY_OF_FRENZY_PROFILE.staminaPercentPerStack,
|
|
perStack: true,
|
|
}],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("scales Determination Rune's source max-health shield and leaves unrelated casts unchanged", () => {
|
|
const determinationRune = sourceAbility(498584);
|
|
const sourceShield = determinationRune.effects.find((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.absorbs?.length);
|
|
expect(sourceShield?.kind).toBe("apply-aura");
|
|
if (sourceShield?.kind !== "apply-aura") throw new Error("Missing Determination Rune shield effect");
|
|
expect(sourceShield.aura.absorbs?.[0]?.amount).toBe(30);
|
|
expect(determinationRune.ranks?.[0]?.effects.find((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.absorbs?.length)).toMatchObject({
|
|
kind: "apply-aura",
|
|
aura: { absorbs: [expect.objectContaining({ amount: 30 })] },
|
|
});
|
|
|
|
const twoThousandHp = castEffects(498584, { casterMaxHealth: 2_000 });
|
|
const fiveThousandHp = castEffects(498584, { casterMaxHealth: 5_000 });
|
|
expect(twoThousandHp.find((effect) => effect.kind === "apply-aura" && effect.aura.absorbs?.length))
|
|
.toMatchObject({ kind: "apply-aura", aura: { absorbs: [expect.objectContaining({ amount: 600 })] } });
|
|
expect(fiveThousandHp.find((effect) => effect.kind === "apply-aura" && effect.aura.absorbs?.length))
|
|
.toMatchObject({ kind: "apply-aura", aura: { absorbs: [expect.objectContaining({ amount: 1_500 })] } });
|
|
|
|
const otherClass = { ...determinationRune, classId: "rom-mage" as const };
|
|
expect(romChampionCastEffects(otherClass, determinationRune.effects, 1, 2_000))
|
|
.toBe(determinationRune.effects);
|
|
const otherPair = { ...determinationRune, secondaryClassId: "rom-mage" as const };
|
|
expect(romChampionCastEffects(otherPair, determinationRune.effects, 1, 2_000))
|
|
.toBe(determinationRune.effects);
|
|
const otherChampionAbility = sourceAbility(498568);
|
|
expect(romChampionCastEffects(otherChampionAbility, otherChampionAbility.effects, 1, 2_000))
|
|
.toBe(otherChampionAbility.effects);
|
|
});
|
|
|
|
it("casts Determination Rune with an absorb equal to 30% of the current max HP", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-champion",
|
|
secondaryClassId: "rom-warrior",
|
|
raceId: "rom-dwarf",
|
|
level: 30,
|
|
});
|
|
const maxHealth = useCombatStore.getState().maxHealth;
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-champion-rom-warrior-elite-30", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
|
|
const shield = useCombatStore.getState().auras.find((aura) => aura.definition.absorbs?.length);
|
|
expect(shield?.definition.absorbs?.[0]?.amount).toBe(Math.round(maxHealth * 0.3));
|
|
});
|
|
|
|
it("keeps Champion cooldown resets source-scoped and removes undefined proc chances", () => {
|
|
const heartRuneResets = castEffects(498651)
|
|
.filter((effect) => effect.kind === "cooldown-reset");
|
|
expect(heartRuneResets.length).toBeGreaterThan(0);
|
|
expect(heartRuneResets.every((effect) => effect.kind === "cooldown-reset"
|
|
&& effect.abilityName === "Rune Siphon")).toBe(true);
|
|
|
|
expect(castEffects(498599).some((effect) => effect.kind === "damage")).toBe(true);
|
|
expect(castEffects(498599).some((effect) => effect.kind === "cooldown-reset")).toBe(false);
|
|
expect(castEffects(498535).some((effect) => effect.kind === "cooldown-reset")).toBe(false);
|
|
});
|
|
|
|
it("cleanses every modeled harmful aura on the party with Righteous Explosion", () => {
|
|
const cleanse = castEffects(499836).find((effect) => effect.kind === "dispel");
|
|
|
|
expect(cleanse).toMatchObject({
|
|
kind: "dispel",
|
|
mode: "cleanse",
|
|
relationship: "friendly",
|
|
scope: "party",
|
|
maxCount: 99,
|
|
});
|
|
});
|
|
});
|