import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { deterministicEnemyCooldown, useCombatStore, } from "./combatStore"; import { WAILING_CAVERNS_DEFINITION } from "./dungeons/wailingCaverns"; import { clearMobRuntimeRegistry, getMobPosition, setMobPosition, } from "./mobRuntimeRegistry"; import { useWailingEncounterStore } from "./wailingCavernsEncounter"; describe("server-backed enemy ability runtime", () => { beforeEach(() => { clearMobRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); }); afterEach(() => { clearMobRuntimeRegistry(); useWailingEncounterStore.getState().reset(); }); it("selects seeded cooldowns reproducibly inside the exact SmartAI range", () => { const first = Array.from({ length: 12 }, (_, activation) => ( deterministicEnemyCooldown("cobrahn", "spell-8040", activation, [10_000, 20_000]) )); const second = Array.from({ length: 12 }, (_, activation) => ( deterministicEnemyCooldown("cobrahn", "spell-8040", activation, [10_000, 20_000]) )); expect(second).toEqual(first); expect(first.every((cooldown) => cooldown >= 10_000 && cooldown <= 20_000)).toBe(true); expect(new Set(first).size).toBeGreaterThan(1); }); it("honors imported fear immunities while leaving Kresh controllable", () => { useCombatStore.getState().registerMob("kresh", { name: "Kresh", serverEntry: 3653, boss: true, maxHealth: 100, }); useCombatStore.getState().registerMob("vancleef", { name: "Edwin VanCleef", serverEntry: 639, boss: true, maxHealth: 100, }); setMobPosition("kresh", [3, 0, 0]); setMobPosition("vancleef", [5, 0, 0]); expect(useCombatStore.getState().castAbility( "priest-psychic-scream", [0, 0, 0], 1_000, ).ok).toBe(true); expect(useCombatStore.getState().mobs.kresh.statuses).toContainEqual({ kind: "fear", sourceAbilityId: "priest-psychic-scream", endsAt: 9_000, }); expect(useCombatStore.getState().mobs.vancleef.statuses).toEqual([]); }); it("heals the lowest-health eligible friendly in range only after the SQL threshold", () => { const combat = WAILING_CAVERNS_DEFINITION.entities["lord-cobrahn"].combat!; useCombatStore.getState().registerMob("cobrahn", { name: "Lord Cobrahn", serverEntry: combat.serverEntry, boss: true, maxHealth: 1_000, attacks: combat.attacks, }); useCombatStore.getState().registerMob("eligible", { name: "Eligible Ally", maxHealth: 1_000, xpReward: 0, }); useCombatStore.getState().registerMob("lower-but-distant", { name: "Distant Ally", maxHealth: 1_000, xpReward: 0, }); setMobPosition("cobrahn", [2, 0, 0]); setMobPosition("eligible", [8, 0, 0]); setMobPosition("lower-but-distant", [60, 0, 0]); useCombatStore.setState((state) => ({ mobs: { ...state.mobs, eligible: { ...state.mobs.eligible, health: 500 }, "lower-but-distant": { ...state.mobs["lower-but-distant"], health: 100 }, }, })); useCombatStore.getState().damageMob("cobrahn", 1, 1_000); useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(4_000, [0, 0, 0]); expect(useCombatStore.getState().mobs.eligible.health).toBeGreaterThan(500); expect(useCombatStore.getState().mobs["lower-but-distant"].health).toBe(100); }); it("casts a non-repeatable health-triggered form exactly once", () => { const combat = WAILING_CAVERNS_DEFINITION.entities["lord-cobrahn"].combat!; useCombatStore.getState().registerMob("cobrahn-form", { name: "Lord Cobrahn", serverEntry: combat.serverEntry, boss: true, maxHealth: 1_000, attacks: combat.attacks.filter((attack) => attack.id === "spell-7965"), }); setMobPosition("cobrahn-form", [2, 0, 0]); useCombatStore.getState().damageMob("cobrahn-form", 750, 1_000); for (const now of [1_000, 1_000, 2_000, 20_000, 40_000, 80_000]) { useCombatStore.getState().advanceMobCombat(now, [0, 0, 0]); } const cobrahn = useCombatStore.getState().mobs["cobrahn-form"]; expect(cobrahn.auras).toContainEqual({ id: "form:serpent", sourceAbilityId: "spell-7965", endsAt: 0, }); expect(cobrahn.abilityCastCounts["spell-7965"]).toBe(1); expect(cobrahn.abilityReadyAt["spell-7965"]).toBe(Number.POSITIVE_INFINITY); }); it("executes data-driven summon effects and removes summons on encounter reset", () => { useCombatStore.getState().registerMob("summoner", { name: "Summoner", maxHealth: 1_000, attacks: [{ id: "summon-vipers", name: "Summon Vipers", delivery: "area", target: "self", school: "nature", animation: "cast", range: 0, cooldownMs: 60_000, initialCooldownMs: [0, 0], repeatCooldownMs: [60_000, 60_000], maximumCasts: 1, damageMultiplier: 0, effect: { kind: "summon", creatureEntry: 5048, count: 2 }, }], }); setMobPosition("summoner", [2, 0, 0]); useCombatStore.getState().damageMob("summoner", 1, 1_000); useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]); const summonIds = Object.keys(useCombatStore.getState().mobs) .filter((id) => id.startsWith("summoner:summon-vipers:summon:1:")); expect(summonIds).toHaveLength(2); for (const id of summonIds) { expect(useCombatStore.getState().mobs[id]).toMatchObject({ serverEntry: 5048, summonedBy: "summoner", engaged: true, xpReward: 0, }); expect(getMobPosition(id)).not.toBeNull(); } useCombatStore.getState().resetEncounter(); expect(Object.keys(useCombatStore.getState().mobs)).toEqual(["summoner"]); expect(summonIds.every((id) => getMobPosition(id) === null)).toBe(true); }); it("uses content-authored summon namespaces and spawn-group ownership", () => { useCombatStore.getState().registerMob("wave-controller", { name: "Wave Controller", maxHealth: 1_000, attacks: [{ id: "release-wave", name: "Release Wave", delivery: "area", target: "self", school: "shadow", animation: "cast", range: 0, cooldownMs: 60_000, initialCooldownMs: [0, 0], repeatCooldownMs: [60_000, 60_000], maximumCasts: 1, damageMultiplier: 0, effect: { kind: "summon", creatureEntry: 5048, count: 2, entityId: "forsaken-abbey:necromage:add", spawnGroupId: "forsaken-abbey:necromage:wave-one", }, }], }); setMobPosition("wave-controller", [2, 0, 0]); useCombatStore.getState().damageMob("wave-controller", 1, 1_000); useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]); const summonIds = Object.keys(useCombatStore.getState().mobs) .filter((id) => id.startsWith("forsaken-abbey:necromage:add:")); expect(summonIds).toEqual([ "forsaken-abbey:necromage:add:1:1", "forsaken-abbey:necromage:add:1:2", ]); expect(summonIds.every((id) => ( useCombatStore.getState().mobs[id].spawnGroupId === "forsaken-abbey:necromage:wave-one" ))).toBe(true); }); it("applies a modular target aura as a dispellable player debuff", () => { useCombatStore.getState().registerMob("runewaker-debuffer", { name: "RuneWaker Debuffer", maxHealth: 1_000, attacks: [{ id: "conductivity", name: "Conductivity", delivery: "projectile", target: "primary", school: "arcane", animation: "cast", castTimeMs: 1_000, range: 20, cooldownMs: 10_000, initialCooldownMs: [0, 0], repeatCooldownMs: [10_000, 10_000], damageMultiplier: 0, effect: { kind: "target-aura", aura: "conductivity", durationMs: 6_000 }, }], }); setMobPosition("runewaker-debuffer", [2, 0, 0]); useCombatStore.getState().damageMob("runewaker-debuffer", 1, 1_000); useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(2_000, [0, 0, 0]); expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({ sourceId: "runewaker-debuffer", targetId: PLAYER_AGGRO_ID, expiresAt: 8_000, definition: expect.objectContaining({ disposition: "debuff", dispelCategory: "magic", }), })); }); it("resolves compound enemy damage and control against the same target", () => { useCombatStore.getState().registerMob("compound-hammer", { name: "Compound Hammer", maxHealth: 1_000, attacks: [{ id: "hammer-pair", name: "Hammer Pair", delivery: "projectile", target: "primary", school: "holy", animation: "cast", castTimeMs: 0, range: 20, cooldownMs: 25_000, initialCooldownMs: [0, 0], repeatCooldownMs: [25_000, 25_000], damageMultiplier: 1.3, effect: { kind: "control", mechanic: "stun", durationMs: 2_500, damageMultiplier: 1.3, }, }], }); setMobPosition("compound-hammer", [2, 0, 0]); useCombatStore.getState().damageMob("compound-hammer", 1, 1_000); const healthBefore = useCombatStore.getState().health; useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]); expect(useCombatStore.getState().health).toBeLessThan(healthBefore); expect(useCombatStore.getState()).toMatchObject({ controlMechanic: "stun", controlledUntil: 3_500, }); expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({ sourceId: "compound-hammer", targetId: PLAYER_AGGRO_ID, expiresAt: 3_500, })); }); it("arms, limits, and consumes a next-melee critical proc only from main-hand autos", () => { useCombatStore.getState().registerMob("melee-proc-boss-1", { name: "Melee Proc Boss", level: 20, maxHealth: 1_000, damageMultiplier: 0.05, attacks: [ { id: "vengeance-driver", name: "Vengeance", delivery: "area", target: "self", school: "holy", animation: "cast", castTimeMs: 0, range: 0, cooldownMs: 1, initialCooldownMs: [0, 0], repeatCooldownMs: [1, 1], maximumCasts: 1, damageMultiplier: 0, effect: { kind: "aura", aura: "vengeance", durationMs: 0, meleeAutoCriticalProc: { chance: 1, triggeredAura: "vengeance-next-melee-critical", durationMs: 6_000, criticalChancePercent: 100, charges: 1, }, }, }, { id: "melee-special", name: "Melee Special", delivery: "melee", target: "primary", school: "physical", animation: "attack", castTimeMs: 0, range: 5, cooldownMs: 1_000, initialCooldownMs: [500, 500], repeatCooldownMs: [1_000, 1_000], damageMultiplier: 1, }, { id: "basic-attack", name: "Basic Attack", delivery: "melee", target: "primary", school: "physical", animation: "attack", castTimeMs: 0, range: 4.5, cooldownMs: 2_000, initialCooldownMs: [1_000, 1_000], repeatCooldownMs: [2_000, 2_000], damageMultiplier: 1, }, ], }); setMobPosition("melee-proc-boss-1", [2, 0, 0]); useCombatStore.getState().damageMob("melee-proc-boss-1", 1, 1_000); useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(1_500, [0, 0, 0]); expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras.map((aura) => aura.id)) .toEqual(["vengeance"]); useCombatStore.getState().advanceMobCombat(2_000, [0, 0, 0]); expect(useCombatStore.getState().lastCombatResult?.landed).toBe(true); expect(useCombatStore.getState().lastCombatResult?.attackTable.criticalPercent).toBe(5); expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras).toContainEqual({ id: "vengeance-next-melee-critical", sourceAbilityId: "vengeance-driver", endsAt: 8_000, nextMeleeCriticalChancePercent: 100, charges: 1, }); useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0]); expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras).toContainEqual( expect.objectContaining({ id: "vengeance-next-melee-critical", endsAt: 8_000, charges: 1 }), ); useCombatStore.getState().advanceMobCombat(4_000, [0, 0, 0]); expect(useCombatStore.getState().lastCombatResult).toMatchObject({ outcome: "crit", critical: true, landed: true, }); expect(useCombatStore.getState().lastCombatResult?.damageBeforeMitigation) .toBe((useCombatStore.getState().lastCombatResult?.rawDamage ?? 0) * 2); expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras).toContainEqual( expect.objectContaining({ id: "vengeance-next-melee-critical", endsAt: 10_000, charges: 1 }), ); useCombatStore.setState((state) => ({ mobs: { ...state.mobs, "melee-proc-boss-1": { ...state.mobs["melee-proc-boss-1"], auras: state.mobs["melee-proc-boss-1"].auras.map((aura) => ( aura.id === "vengeance" ? { ...aura, meleeAutoCriticalProc: undefined } : aura )), }, }, })); useCombatStore.getState().advanceMobCombat(5_000, [0, 0, 0]); useCombatStore.getState().advanceMobCombat(6_000, [0, 0, 0]); expect(useCombatStore.getState().lastCombatResult?.critical).toBe(true); expect(useCombatStore.getState().mobs["melee-proc-boss-1"].auras.some( (aura) => aura.id === "vengeance-next-melee-critical", )).toBe(false); }); it("honors encounter-state conditions before selecting an ability", () => { useWailingEncounterStore.getState().reset(); useCombatStore.getState().registerMob("event-caster", { name: "Event Caster", maxHealth: 1_000, attacks: [{ id: "event-strike", name: "Event Strike", delivery: "projectile", target: "primary", school: "shadow", animation: "cast", range: 30, cooldownMs: 10_000, initialCooldownMs: [0, 0], repeatCooldownMs: [10_000, 10_000], damageMultiplier: 1, effect: { kind: "damage", multiplier: 1 }, condition: { kind: "encounter-state", encounterId: "wailing-caverns:naralex", states: ["mutanus"], }, }], }); setMobPosition("event-caster", [2, 0, 0]); useCombatStore.getState().damageMob("event-caster", 1, 1_000); const initialHealth = useCombatStore.getState().health; expect(useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0])).toBe(0); useWailingEncounterStore.setState({ phase: "mutanus" }); expect(useCombatStore.getState().advanceMobCombat(1_000, [0, 0, 0])).toBe(0); expect(useCombatStore.getState().advanceMobCombat(2_500, [0, 0, 0])).toBeGreaterThan(0); expect(useCombatStore.getState().health).toBeLessThan(initialHealth); }); });