import { describe, expect, it } from "vitest"; import { MANASTORM_PROGRESS_VERSION, applyManastormEndgameHandoff, applyManastormRewards, createEmptyManastormProgress, hasClaimedManastormReward, hasClearedManastormLevel, manastormModeProgress, manastormPartyProgress, mergeManastormUnlockedContent, normalizeManastormProgress, recordManastormCheckpoint, recordManastormFailure, setManastormCacheMeter, setManastormLoadout, setManastormRewardCaches, validateManastormLoadout, } from "./manastormProgress"; describe("persisted Manastorm progression", () => { it("keeps clears and checkpoints independent for every party size", () => { let progress = createEmptyManastormProgress(); progress = recordManastormCheckpoint(progress, 1, 5, 6); progress = recordManastormCheckpoint(progress, 5, 10, 11); expect(manastormPartyProgress(progress, 1)).toEqual({ highestLevel: 5, checkpointLevel: 6, firstClearLevels: [5], }); expect(manastormPartyProgress(progress, 2)).toEqual({ highestLevel: 0, checkpointLevel: 1, firstClearLevels: [], }); expect(manastormPartyProgress(progress, 5)).toEqual({ highestLevel: 10, checkpointLevel: 11, firstClearLevels: [10], }); }); it("keeps leveling and end-game records independent", () => { let progress = createEmptyManastormProgress(); progress = recordManastormCheckpoint(progress, 3, 5, 6, 0); progress = recordManastormCheckpoint(progress, 3, 101, 101, 2); progress = setManastormCacheMeter(progress, 4, 0); progress = setManastormCacheMeter(progress, 2, 2); expect(manastormPartyProgress(progress, 3, 0)).toEqual({ highestLevel: 5, checkpointLevel: 6, firstClearLevels: [5], }); expect(manastormPartyProgress(progress, 3, 2)).toEqual({ highestLevel: 101, checkpointLevel: 101, firstClearLevels: [101], }); expect(manastormModeProgress(progress, 0).cacheMeter).toBe(4); expect(manastormModeProgress(progress, 2).cacheMeter).toBe(2); expect(progress.cacheMeter).toBe(4); }); it("migrates a v1 record into mode 0 and leaves mode 2 empty", () => { const migrated = normalizeManastormProgress({ version: 1, partySizes: { 1: { highestLevel: 5, checkpointLevel: 6, firstClearLevels: [5] }, }, unlockedContent: ["leveling-map"], currencies: { bedlamBullions: 3, bonzoBolts: 7 }, cacheMeter: 4, rewardCaches: [{ id: "legacy-cache", cacheId: "bronze", rewardId: "legacy-reward", stageId: "legacy-stage", level: 5, }], loadout: ["leveling-map"], claimedRewards: ["legacy-first-clear"], }); expect(migrated.version).toBe(MANASTORM_PROGRESS_VERSION); expect(manastormModeProgress(migrated, 0)).toMatchObject({ unlockedContent: ["leveling-map"], cacheMeter: 4, claimedRewards: ["legacy-first-clear"], rewardCaches: [expect.objectContaining({ id: "legacy-cache", modeId: 0 })], }); expect(manastormPartyProgress(migrated, 1, 0)).toMatchObject({ highestLevel: 5, checkpointLevel: 6, firstClearLevels: [5], }); expect(manastormModeProgress(migrated, 2)).toEqual({ partySizes: expect.any(Object), unlockedContent: [], cacheMeter: 0, rewardCaches: [], claimedRewards: [], }); expect(manastormPartyProgress(migrated, 1, 2)).toEqual({ highestLevel: 0, checkpointLevel: 1, firstClearLevels: [], }); expect(migrated.currencies).toEqual({ bedlamBullions: 3, bonzoBolts: 7 }); expect(migrated.loadout).toEqual(["leveling-map"]); const serialized = JSON.parse(JSON.stringify(migrated)); expect(serialized).not.toHaveProperty("partySizes"); expect(serialized).not.toHaveProperty("unlockedContent"); expect(serialized).not.toHaveProperty("cacheMeter"); expect(serialized.modes["0"]).toMatchObject({ unlockedContent: ["leveling-map"], cacheMeter: 4, }); expect(normalizeManastormProgress(serialized)).toEqual(migrated); }); it("applies the end-game checkpoint handoff once without copying first clears", () => { let progress = createEmptyManastormProgress(); progress = recordManastormCheckpoint(progress, 2, 105, 106, 0); progress = recordManastormCheckpoint(progress, 3, 100, 101, 0); progress = applyManastormRewards(progress, { id: "leveling-only", unlockedContent: ["leveling-map"], }, 0); const handedOff = applyManastormEndgameHandoff(progress); expect(handedOff.endgameHandoffApplied).toBe(true); expect(manastormPartyProgress(handedOff, 2, 2)).toEqual({ highestLevel: 105, checkpointLevel: 106, firstClearLevels: [], }); expect(manastormPartyProgress(handedOff, 3, 2)).toEqual({ highestLevel: 100, checkpointLevel: 101, firstClearLevels: [], }); expect(manastormModeProgress(handedOff, 2)).toMatchObject({ unlockedContent: [], cacheMeter: 0, rewardCaches: [], claimedRewards: [], }); const advancedLeveling = recordManastormCheckpoint(handedOff, 2, 110, 111, 0); const secondHandoff = applyManastormEndgameHandoff(advancedLeveling); expect(manastormPartyProgress(secondHandoff, 2, 2).checkpointLevel).toBe(106); }); it("namespaces claimed rewards and persisted caches by mode while sharing currencies", () => { let progress = createEmptyManastormProgress(); progress = applyManastormRewards(progress, { id: "same-first-clear", bedlamBullions: 3, unlockedContent: ["leveling-unlock"], }, 0); progress = applyManastormRewards(progress, { id: "same-first-clear", bedlamBullions: 3, unlockedContent: ["endgame-unlock"], }, 2); progress = setManastormRewardCaches(progress, [{ id: "cache-2", cacheId: "endgame-cache", rewardId: "same-first-clear", stageId: "manastorm:stage:2:409:4229", level: 221, }], 2); expect(hasClaimedManastormReward(progress, "same-first-clear", 0)).toBe(true); expect(hasClaimedManastormReward(progress, "same-first-clear", 2)).toBe(true); expect(manastormModeProgress(progress, 0).unlockedContent).toEqual(["leveling-unlock"]); expect(manastormModeProgress(progress, 2).unlockedContent).toEqual(["endgame-unlock"]); expect(manastormModeProgress(progress, 2).rewardCaches).toEqual([ expect.objectContaining({ id: "cache-2", modeId: 2 }), ]); expect(progress.currencies.bedlamBullions).toBe(6); }); it("resumes a failed milestone from the saved checkpoint without erasing clears", () => { let progress = createEmptyManastormProgress(); progress = recordManastormCheckpoint(progress, 3, 5, 6); progress = recordManastormCheckpoint(progress, 3, 6); const failure = recordManastormFailure(progress, 3, 10); expect(failure.resumeLevel).toBe(6); expect(hasClearedManastormLevel(failure.progress, 3, 5)).toBe(true); expect(hasClearedManastormLevel(failure.progress, 3, 6)).toBe(true); expect(manastormPartyProgress(failure.progress, 3).highestLevel).toBe(6); }); it("grants a stable first-clear reward only once", () => { let progress = createEmptyManastormProgress(); progress = recordManastormCheckpoint(progress, 2, 5, 6); progress = applyManastormRewards(progress, { id: "manastorm:first-clear:2:5", bedlamBullions: 3, bonzoBolts: 7, unlockedContent: ["storm-choice-a"], }); const firstGrant = progress; progress = recordManastormCheckpoint(progress, 2, 5, 6); progress = applyManastormRewards(progress, { id: "manastorm:first-clear:2:5", bedlamBullions: 3, bonzoBolts: 7, unlockedContent: ["storm-choice-a"], }); expect(hasClaimedManastormReward(progress, "manastorm:first-clear:2:5")).toBe(true); expect(progress.currencies).toEqual({ bedlamBullions: 3, bonzoBolts: 7 }); expect(progress.unlockedContent).toEqual(["storm-choice-a"]); expect(progress).toEqual(firstGrant); }); it("rejects locked, duplicate, and oversized loadouts and persists a valid one", () => { const unlocked = ["storm-choice-a", "storm-choice-b"]; expect(validateManastormLoadout(["storm-choice-a", "locked"], unlocked, 2)) .toMatchObject({ ok: false, error: "locked has not been unlocked." }); expect(validateManastormLoadout(["storm-choice-a", "storm-choice-a"], unlocked, 2)) .toMatchObject({ ok: false, error: "storm-choice-a is selected more than once." }); expect(validateManastormLoadout(unlocked, unlocked, 1)) .toMatchObject({ ok: false, error: "Loadout cannot contain more than 1 selections." }); const progress = applyManastormRewards(createEmptyManastormProgress(), { id: "unlock-loadout", unlockedContent: unlocked, }); const update = setManastormLoadout(progress, ["storm-choice-b", "storm-choice-a"], 2); expect(update).toMatchObject({ ok: true, loadout: ["storm-choice-b", "storm-choice-a"], }); expect(update.progress.loadout).toEqual(["storm-choice-b", "storm-choice-a"]); }); it("merges client-grounded spell choices without recording a reward claim", () => { const progress = mergeManastormUnlockedContent( createEmptyManastormProgress(), ["priest-smite", "priest-flash-heal", "priest-smite"], ); const update = setManastormLoadout(progress, ["priest-flash-heal"], 4); expect(progress.unlockedContent).toEqual(["priest-smite", "priest-flash-heal"]); expect(progress.claimedRewards).toEqual([]); expect(update).toMatchObject({ ok: true, loadout: ["priest-flash-heal"], progress: { loadout: ["priest-flash-heal"] }, }); }); });