289 lines
9.6 KiB
TypeScript
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 });
|
|
});
|
|
});
|