import { afterEach, describe, expect, it } from "vitest"; import type { AbilityDefinition } from "./abilityCatalog"; import { applyAbilityBalanceTuning, currentBalanceTuningPolicy, loadBalanceTuningPolicy, normalizeBalanceTuningPolicy, resetBalanceTuningCache, saveBalanceTuningPolicy, } from "./balanceTuning"; const ability: AbilityDefinition = { id: "test-fireball", classId: "mage", dbcSpellId: 1, name: "Test Fireball", unlockLevel: 1, icon: "", sigil: "TF", description: "Test", target: "hostile", range: { min: 0, max: 30 }, radius: 8, castMode: "cast", castTimeMs: 2_000, cooldownMs: 10_000, gcdMs: 1_500, cost: { resource: "mana", amount: 20 }, effects: [ { kind: "damage", coefficient: 2, basePoints: 100 }, { kind: "dot", coefficient: 0.5, ticks: 4, intervalMs: 1_000 }, { kind: "utility", action: "class-script", effectType: 1, auraType: 0, miscValue: 0, triggerSpellId: 0, durationMs: -1 }, ], }; function memoryStorage(initial: string | null = null) { let value = initial; return { getItem: () => value, setItem: (_key: string, next: string) => { value = next; }, removeItem: () => { value = null; }, value: () => value, }; } describe("local ability and talent balance tuning", () => { afterEach(() => resetBalanceTuningCache()); it("normalizes unsafe values and drops default-only overrides", () => { expect(normalizeBalanceTuningPolicy({ abilityOverrides: { default: { cooldownPercent: 100 }, tuned: { cooldownPercent: -50, damagePercent: 9_999 }, }, talentOverrides: { talent: { effectScalePercent: 150, modifiersPerRank: { "critical-chance-percent": 2.5 } }, }, })).toEqual({ schemaVersion: 1, abilityOverrides: { tuned: { cooldownPercent: 0, damagePercent: 1_000 } }, talentOverrides: { talent: { effectScalePercent: 150, modifiersPerRank: { "critical-chance-percent": 2.5 } }, }, }); }); it("applies cooldown, cast, cost, damage, duration, and radius tuning", () => { const tuned = applyAbilityBalanceTuning(ability, normalizeBalanceTuningPolicy({ abilityOverrides: { [ability.id]: { cooldownPercent: 50, castTimePercent: 0, resourceCostPercent: 75, damagePercent: 125, durationPercent: 150, radiusPercent: 200, }, }, })); expect(tuned).toMatchObject({ castMode: "instant", castTimeMs: 0, cooldownMs: 5_000, cost: { amount: 15 }, radius: 16, }); expect(tuned.effects[0]).toMatchObject({ coefficient: 2.5, basePoints: 125 }); expect(tuned.effects[1]).toMatchObject({ coefficient: 0.625, ticks: 6 }); expect(tuned.effects[2]).toMatchObject({ durationMs: -1 }); }); it("persists valid policy data and falls back after corrupt storage", () => { const storage = memoryStorage(); saveBalanceTuningPolicy({ abilityOverrides: { x: { cooldownPercent: 80 } } }, storage); expect(JSON.parse(storage.value()!)).toMatchObject({ abilityOverrides: { x: { cooldownPercent: 80 } } }); expect(currentBalanceTuningPolicy().abilityOverrides.x.cooldownPercent).toBe(80); expect(loadBalanceTuningPolicy(memoryStorage("not-json"))).toEqual({ schemaVersion: 1, abilityOverrides: {}, talentOverrides: {}, }); }); });