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 { 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); }); });