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

289 lines
9.6 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import {
createCharacter,
createOfflineSession,
listCharacters,
updateCharacterManastormProgress,
type StorageLike,
} from "../app/accountRepository";
import {
getManastormCatalog,
setManastormCatalog,
} from "./manastorm";
import { INSTALLED_MANASTORM_RUNTIME_CATALOG } from "./manastormInstalledCatalog";
import {
createEmptyManastormProgress,
hasClaimedManastormReward,
manastormModeProgress,
manastormPartyProgress,
recordManastormFailure,
type ManastormProgress,
} from "./manastormProgress";
import {
synchronizeManastormProgress,
type ManastormProgressRuntimeSnapshot,
} from "./manastormProgressSync";
import { useManastormStore } from "./manastormStore";
class MemoryStorage implements StorageLike {
private values = new Map<string, string>();
getItem(key: string) { return this.values.get(key) ?? null; }
setItem(key: string, value: string) { this.values.set(key, value); }
removeItem(key: string) { this.values.delete(key); }
}
function runtimeSnapshot(): ManastormProgressRuntimeSnapshot {
return useManastormStore.getState();
}
function saveRuntimeDelta(
progress: ManastormProgress,
previous: ManastormProgressRuntimeSnapshot,
): ManastormProgress {
return synchronizeManastormProgress(
progress,
previous,
runtimeSnapshot(),
getManastormCatalog(),
).progress;
}
function clearCurrentLevel(progress: ManastormProgress): ManastormProgress {
useManastormStore.getState().enterStage();
useManastormStore.getState().observeTrash(true);
let previous = runtimeSnapshot();
useManastormStore.getState().observeBoss(true, true);
let nextProgress = saveRuntimeDelta(progress, previous);
previous = runtimeSnapshot();
expect(useManastormStore.getState().beginTransition()).toBe(true);
expect(useManastormStore.getState().prepareNextEncounter(() => 0.31)).not.toBeNull();
nextProgress = saveRuntimeDelta(nextProgress, previous);
useManastormStore.getState().finishTransition();
return nextProgress;
}
describe("Manastorm checkpoint and reward persistence", () => {
beforeEach(() => {
setManastormCatalog(INSTALLED_MANASTORM_RUNTIME_CATALOG);
useManastormStore.getState().resetRun();
});
it("survives five clears, profile reload, a pre-10 wipe, and checkpoint resume", () => {
const storage = new MemoryStorage();
const session = createOfflineSession();
const created = createCharacter({
ownerId: session.ownerId,
name: "Stormkeeper",
raceId: "human",
classId: "priest",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
let progress = created.manastormProgress!;
useManastormStore.getState().startRun({
partySize: 3,
startingLevel: 1,
cacheMeter: progress.cacheMeter,
rewardCaches: progress.rewardCaches,
random: () => 0.31,
});
for (let level = 1; level <= 5; level += 1) {
expect(useManastormStore.getState().level).toBe(level);
progress = clearCurrentLevel(progress);
expect(updateCharacterManastormProgress(
session.ownerId,
created.id,
progress,
storage,
)).not.toBeNull();
}
const afterFive = listCharacters(session.ownerId, storage)[0]!.manastormProgress!;
expect(manastormPartyProgress(afterFive, 3)).toEqual({
highestLevel: 5,
checkpointLevel: 6,
firstClearLevels: [1, 2, 3, 4, 5],
});
expect(afterFive.currencies.bonzoBolts).toBeGreaterThan(0);
expect(afterFive.currencies.bedlamBullions).toBeGreaterThan(0);
expect(afterFive.rewardCaches).toHaveLength(5);
expect(afterFive.rewardCaches).toEqual(expect.arrayContaining([
expect.objectContaining({
cacheId: "leveling-manastorm-cache",
level: 5,
}),
]));
expect(hasClaimedManastormReward(afterFive, "manastorm:unlocks:0:3:5", 0)).toBe(true);
expect(hasClaimedManastormReward(
afterFive,
"manastorm:first-clear:0:3:1:installed-leveling-first-clear",
0,
)).toBe(true);
expect(afterFive.unlockedContent).toEqual(expect.arrayContaining([
expect.stringMatching(/Moonbrook/),
]));
// Simulate a full runtime wipe and profile reconstruction.
useManastormStore.getState().resetRun();
let reloaded = listCharacters(session.ownerId, storage)[0]!.manastormProgress!;
useManastormStore.getState().startRun({
partySize: 3,
startingLevel: manastormPartyProgress(reloaded, 3).checkpointLevel,
cacheMeter: reloaded.cacheMeter,
rewardCaches: reloaded.rewardCaches,
random: () => 0.31,
});
expect(useManastormStore.getState()).toMatchObject({
level: 6,
checkpointLevel: 6,
rewardCache: expect.arrayContaining([
expect.objectContaining({ cacheId: "leveling-manastorm-cache" }),
]),
});
// Clear 6-9, then wipe on level 10 before earning its clear.
for (let level = 6; level <= 9; level += 1) {
expect(useManastormStore.getState().level).toBe(level);
reloaded = clearCurrentLevel(reloaded);
expect(updateCharacterManastormProgress(
session.ownerId,
created.id,
reloaded,
storage,
)).not.toBeNull();
}
expect(useManastormStore.getState().level).toBe(10);
expect(useManastormStore.getState().failRun()).toBe(true);
const beforeWipe = listCharacters(session.ownerId, storage)[0]!.manastormProgress!;
const failure = recordManastormFailure(beforeWipe, 3, 10);
expect(failure.resumeLevel).toBe(6);
expect(manastormPartyProgress(failure.progress, 3)).toMatchObject({
highestLevel: 9,
checkpointLevel: 6,
firstClearLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
});
expect(failure.progress.currencies.bonzoBolts).toBeGreaterThan(afterFive.currencies.bonzoBolts);
expect(failure.progress.currencies.bedlamBullions).toBe(afterFive.currencies.bedlamBullions);
expect(failure.progress.cacheMeter).toBe(0);
expect(failure.progress.rewardCaches).toHaveLength(9);
expect(failure.progress.rewardCaches.slice(0, 5)).toEqual(afterFive.rewardCaches);
expect(hasClaimedManastormReward(failure.progress, "manastorm:unlocks:0:3:5", 0)).toBe(true);
expect(hasClaimedManastormReward(failure.progress, "manastorm:unlocks:0:3:6", 0)).toBe(true);
for (const partySize of [1, 2, 4, 5] as const) {
expect(manastormPartyProgress(failure.progress, partySize)).toEqual({
highestLevel: 0,
checkpointLevel: 1,
firstClearLevels: [],
});
}
const reconstructed = listCharacters(session.ownerId, storage)[0]!.manastormProgress!;
const failedRuntime = runtimeSnapshot();
useManastormStore.getState().resetRun();
const resetSync = synchronizeManastormProgress(
reconstructed,
failedRuntime,
runtimeSnapshot(),
getManastormCatalog(),
);
expect(resetSync.progress.rewardCaches).toEqual(reconstructed.rewardCaches);
expect(manastormModeProgress(resetSync.progress, 2).rewardCaches).toEqual([]);
useManastormStore.getState().startRun({
partySize: failure.partySize,
startingLevel: failure.resumeLevel,
cacheMeter: resetSync.progress.cacheMeter,
rewardCaches: resetSync.progress.rewardCaches,
random: () => 0.31,
});
expect(useManastormStore.getState()).toMatchObject({
level: 6,
checkpointLevel: 6,
cacheMeter: 0,
rewardCache: expect.arrayContaining([
expect.objectContaining({ cacheId: "leveling-manastorm-cache" }),
]),
});
});
it("syncs an end-game run only into mode 2 while currencies remain shared", () => {
const previous: ManastormProgressRuntimeSnapshot = {
status: "fighting",
completed: 100,
partySize: 2,
modeId: 2,
currency: { bedlamBullions: 4, bonzoBolts: 10 },
cacheMeter: 1,
rewardCache: [],
};
const state: ManastormProgressRuntimeSnapshot = {
status: "portal-ready",
completed: 101,
partySize: 2,
modeId: 2,
currency: { bedlamBullions: 7, bonzoBolts: 12 },
cacheMeter: 2,
rewardCache: [{
id: "endgame-cache-instance",
cacheId: "endgame-cache",
rewardId: "endgame-clear",
stageId: "manastorm:stage:2:409:4229",
level: 101,
}],
};
const catalog = {
messages: [
{
modeId: 0,
progressionValue: 101,
title: "Leveling unlock",
description: "Must not cross modes.",
},
{
modeId: 2,
progressionValue: 101,
title: "End-game unlock",
description: "Mode-local.",
},
],
};
const synced = synchronizeManastormProgress(
createEmptyManastormProgress(),
previous,
state,
catalog,
).progress;
expect(manastormPartyProgress(synced, 2, 0)).toEqual({
highestLevel: 0,
checkpointLevel: 1,
firstClearLevels: [],
});
expect(manastormPartyProgress(synced, 2, 2)).toEqual({
highestLevel: 101,
checkpointLevel: 1,
firstClearLevels: [101],
});
expect(manastormModeProgress(synced, 0)).toMatchObject({
unlockedContent: [],
cacheMeter: 0,
rewardCaches: [],
claimedRewards: [],
});
expect(manastormModeProgress(synced, 2)).toMatchObject({
unlockedContent: [expect.stringMatching(/End-game unlock/)],
cacheMeter: 2,
rewardCaches: [expect.objectContaining({
id: "endgame-cache-instance",
modeId: 2,
})],
claimedRewards: ["manastorm:unlocks:2:2:101"],
});
expect(synced.currencies).toEqual({ bedlamBullions: 3, bonzoBolts: 2 });
});
});