Files
healer-man/src/game/manastormProgress.test.ts
T
2026-08-14 15:56:39 -04:00

263 lines
9.6 KiB
TypeScript

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"] },
});
});
});