126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { AbilityDefinition } from "./abilityCatalog";
|
|
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
|
import { romEffectsForCast } from "./romCombatMechanics";
|
|
import {
|
|
ROM_WARLOCK_ROGUE_SHADOW_SLICE,
|
|
romWarlockRogueShadowSliceEffects,
|
|
} from "./romWarlockRogueShadowSlice";
|
|
|
|
function shadowStep(overrides: Partial<AbilityDefinition> = {}): AbilityDefinition {
|
|
return {
|
|
id: "rom-rogue-general-7",
|
|
classId: "rom-rogue",
|
|
dbcSpellId: ROM_WARLOCK_ROGUE_SHADOW_SLICE.triggerSpellId,
|
|
name: "Shadow Step",
|
|
unlockLevel: 28,
|
|
icon: "",
|
|
sigil: "",
|
|
description: "Instantly move behind the target.",
|
|
target: "hostile",
|
|
range: { min: 0, max: 10 },
|
|
castMode: "instant",
|
|
castTimeMs: 0,
|
|
cooldownMs: 20_000,
|
|
gcdMs: 1_000,
|
|
cost: { resource: "energy", amount: 20 },
|
|
effects: [],
|
|
source: "runewaker",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const emptyCastContext = {
|
|
sealStacks: 0,
|
|
bleedCount: 0,
|
|
charged: false,
|
|
mainHandDagger: false,
|
|
casterMaxHealth: 1_000,
|
|
targetHealthPercent: 1,
|
|
} as const;
|
|
|
|
function catalogShadowStep(): AbilityDefinition {
|
|
const ability = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === 490_344);
|
|
expect(ability).toBeDefined();
|
|
return ability!;
|
|
}
|
|
|
|
describe("Warlock/Rogue Shadow Slice source helper", () => {
|
|
it("adds the exact CoA strike, stun, buffs, and Psi rider only with the matching passive", () => {
|
|
const effects = romWarlockRogueShadowSliceEffects(
|
|
shadowStep(),
|
|
[ROM_WARLOCK_ROGUE_SHADOW_SLICE.passiveSpellId],
|
|
);
|
|
|
|
expect(effects).toEqual([
|
|
{
|
|
kind: "damage",
|
|
coefficient: 0,
|
|
basePoints: 4_400,
|
|
school: "shadow",
|
|
},
|
|
{ kind: "status", status: "stun", durationMs: 1_000 },
|
|
expect.objectContaining({
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: expect.objectContaining({
|
|
id: "rom-warlock-rogue-shadow-slice-498678",
|
|
durationMs: 30_000,
|
|
modifiers: expect.arrayContaining([
|
|
expect.objectContaining({ kind: "damage", school: "shadow", value: 0.3 }),
|
|
expect.objectContaining({ kind: "stat", stat: "movement-speed", value: 0.1 }),
|
|
]),
|
|
}),
|
|
}),
|
|
{ kind: "resource", resource: "psi", amount: 3 },
|
|
]);
|
|
});
|
|
|
|
it("does not map by name, passive ID alone, or another spell/class identity", () => {
|
|
const passive = [ROM_WARLOCK_ROGUE_SHADOW_SLICE.passiveSpellId];
|
|
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep(), [])).toBeNull();
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep({ dbcSpellId: 490345 }), passive)).toBeNull();
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep({ classId: "rom-warlock" }), passive)).toBeNull();
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep({ secondaryClassId: "rom-priest" }), passive)).toBeNull();
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep({ name: "Renamed ability" }), passive)).toBeNull();
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep({ source: "coa-tree" }), passive)).toBeNull();
|
|
});
|
|
|
|
it("integrates the Warlock/Rogue passive while preserving ordinary Shadow Step without it", () => {
|
|
const ability = catalogShadowStep();
|
|
const passive = ROM_ALL_ABILITIES.find((candidate) => candidate.dbcSpellId === 498_678);
|
|
expect(passive).toMatchObject({
|
|
classId: "rom-warlock",
|
|
secondaryClassId: "rom-rogue",
|
|
name: "Shadow Slice",
|
|
});
|
|
expect(passive?.description).toContain("4,400 Dark damage");
|
|
expect(ability.description).toContain("In the Warlock/Rogue build with Shadow Slice (498678)");
|
|
expect(ability.description).toContain("cannot model the behind-target positioning");
|
|
expect(ability.costs).toEqual([{ resource: "energy", amount: 20 }]);
|
|
expect(ability.cooldownMs).toBe(20_000);
|
|
|
|
const ordinaryEffects = romEffectsForCast(ability, emptyCastContext);
|
|
expect(ordinaryEffects).toEqual(ability.effects);
|
|
expect(ordinaryEffects).toContainEqual(expect.objectContaining({ kind: "movement", movement: "teleport" }));
|
|
|
|
const shadowSliceEffects = romEffectsForCast(ability, {
|
|
...emptyCastContext,
|
|
learnedPassiveSpellIds: [ROM_WARLOCK_ROGUE_SHADOW_SLICE.passiveSpellId],
|
|
});
|
|
expect(shadowSliceEffects).toEqual(romWarlockRogueShadowSliceEffects(
|
|
ability,
|
|
[ROM_WARLOCK_ROGUE_SHADOW_SLICE.passiveSpellId],
|
|
));
|
|
expect(shadowSliceEffects?.some((effect) => effect.kind === "movement")).toBe(false);
|
|
});
|
|
|
|
it("records that Shadow Slice replaces the local teleport instead of adding another movement effect", () => {
|
|
expect(ROM_WARLOCK_ROGUE_SHADOW_SLICE.replacesShadowStepTeleport).toBe(true);
|
|
expect(romWarlockRogueShadowSliceEffects(shadowStep(), [498678])?.some((effect) => (
|
|
effect.kind === "movement"
|
|
))).toBe(false);
|
|
});
|
|
});
|