162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
import {
|
|
manastormGroupModifier,
|
|
type ManastormCatalogLike,
|
|
type ManastormRunState,
|
|
} from "./manastorm";
|
|
import {
|
|
addManastormCurrencies,
|
|
applyManastormRewards,
|
|
hasClearedManastormLevel,
|
|
manastormModeProgress,
|
|
normalizeManastormProgress,
|
|
normalizeManastormModeId,
|
|
recordManastormCheckpoint,
|
|
setManastormCacheMeter,
|
|
setManastormRewardCaches,
|
|
type ManastormProgress,
|
|
} from "./manastormProgress";
|
|
|
|
export type ManastormProgressRuntimeSnapshot = Pick<
|
|
ManastormRunState,
|
|
"status" | "completed" | "partySize" | "modeId" | "currency" | "cacheMeter" | "rewardCache"
|
|
>;
|
|
|
|
export interface ManastormProgressSyncResult {
|
|
readonly progress: ManastormProgress;
|
|
readonly changed: boolean;
|
|
}
|
|
|
|
function firstClearReward(
|
|
catalog: ManastormCatalogLike,
|
|
level: number,
|
|
partySize: ManastormRunState["partySize"],
|
|
modeId: number,
|
|
) {
|
|
const definition = (catalog.rewards ?? []).find((reward) => (
|
|
reward.claimPolicy === "first-clear"
|
|
&& (!reward.modeIds?.length || reward.modeIds.includes(modeId))
|
|
&& (reward.minimumLevel ?? 1) <= level
|
|
&& (reward.maximumLevel ?? Number.POSITIVE_INFINITY) >= level
|
|
&& (reward.minimumPartySize ?? 1) <= partySize
|
|
&& (reward.maximumPartySize ?? 5) >= partySize
|
|
&& (reward.milestoneLevel === undefined || reward.milestoneLevel === level)
|
|
&& (reward.milestoneEvery === undefined
|
|
|| level % Math.max(1, Math.trunc(reward.milestoneEvery)) === 0)
|
|
));
|
|
if (!definition) return null;
|
|
const group = manastormGroupModifier(partySize, catalog, modeId);
|
|
const amount = Math.max(0, Math.round((
|
|
(definition.currencyBase ?? 0)
|
|
+ (definition.currencyPerLevel ?? 0) * Math.max(0, level - 1)
|
|
) * group.rewardMultiplier));
|
|
return {
|
|
id: `manastorm:first-clear:${modeId}:${partySize}:${level}:${definition.id}`,
|
|
...(definition.currencyId === "bedlamBullions" ? { bedlamBullions: amount } : {}),
|
|
...(definition.currencyId === "bonzoBolts" ? { bonzoBolts: amount } : {}),
|
|
...(definition.cacheMeter ? { cacheMeter: definition.cacheMeter } : {}),
|
|
};
|
|
}
|
|
|
|
function sameCaches(
|
|
left: ReturnType<typeof manastormModeProgress>["rewardCaches"],
|
|
right: ManastormRunState["rewardCache"],
|
|
modeId: number,
|
|
): boolean {
|
|
const safeModeId = normalizeManastormModeId(modeId);
|
|
return left.length === right.length && left.every((cache, index) => {
|
|
const candidate = right[index];
|
|
return Boolean(candidate)
|
|
&& cache.modeId === safeModeId
|
|
&& cache.id === candidate.id
|
|
&& cache.cacheId === candidate.cacheId
|
|
&& cache.rewardId === candidate.rewardId
|
|
&& cache.stageId === candidate.stageId
|
|
&& cache.level === candidate.level
|
|
&& cache.rewardMultiplier === (candidate.rewardMultiplier ?? 1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pure save-boundary reducer shared by React and headless integration tests.
|
|
* Runtime currency values are run-local totals, so only positive deltas are
|
|
* added; checkpoints, first-clear unlocks, cache meter, and unopened caches
|
|
* are copied into durable profile progress.
|
|
*/
|
|
export function synchronizeManastormProgress(
|
|
progressValue: ManastormProgress,
|
|
previous: ManastormProgressRuntimeSnapshot,
|
|
state: ManastormProgressRuntimeSnapshot,
|
|
catalog: ManastormCatalogLike,
|
|
): ManastormProgressSyncResult {
|
|
const original = normalizeManastormProgress(progressValue);
|
|
let progress = original;
|
|
|
|
if (state.completed > previous.completed) {
|
|
const isFirstClear = !hasClearedManastormLevel(
|
|
original,
|
|
state.partySize,
|
|
state.completed,
|
|
state.modeId,
|
|
);
|
|
progress = recordManastormCheckpoint(
|
|
progress,
|
|
state.partySize,
|
|
state.completed,
|
|
state.completed % 5 === 0 ? state.completed + 1 : undefined,
|
|
state.modeId,
|
|
);
|
|
if (isFirstClear) {
|
|
const reward = firstClearReward(
|
|
catalog,
|
|
state.completed,
|
|
state.partySize,
|
|
state.modeId,
|
|
);
|
|
if (reward) progress = applyManastormRewards(progress, reward, state.modeId);
|
|
}
|
|
const unlockedContent = (catalog.messages ?? [])
|
|
.filter((message) => (
|
|
message.modeId === state.modeId
|
|
&& message.progressionValue === state.completed
|
|
))
|
|
.map((message) => `manastorm:unlock:${message.progressionValue}:${message.groupId ?? 0}:${message.title}`);
|
|
if (unlockedContent.length) {
|
|
progress = applyManastormRewards(progress, {
|
|
id: `manastorm:unlocks:${state.modeId}:${state.partySize}:${state.completed}`,
|
|
unlockedContent,
|
|
}, state.modeId);
|
|
}
|
|
}
|
|
|
|
const bedlamDelta = Math.max(
|
|
0,
|
|
(state.currency.bedlamBullions ?? 0) - (previous.currency.bedlamBullions ?? 0),
|
|
);
|
|
const bonzoDelta = Math.max(
|
|
0,
|
|
(state.currency.bonzoBolts ?? 0) - (previous.currency.bonzoBolts ?? 0),
|
|
);
|
|
if (bedlamDelta || bonzoDelta) {
|
|
progress = addManastormCurrencies(progress, {
|
|
bedlamBullions: bedlamDelta,
|
|
bonzoBolts: bonzoDelta,
|
|
});
|
|
}
|
|
if (state.status !== "idle" && state.cacheMeter !== previous.cacheMeter) {
|
|
progress = setManastormCacheMeter(progress, state.cacheMeter, state.modeId);
|
|
}
|
|
// resetRun publishes an idle state before the game surface unmounts. That is
|
|
// not a cache-open action and must never erase unopened durable caches.
|
|
const modeProgress = manastormModeProgress(progress, state.modeId);
|
|
if (
|
|
state.status !== "idle"
|
|
&& !sameCaches(modeProgress.rewardCaches, state.rewardCache, state.modeId)
|
|
) {
|
|
progress = setManastormRewardCaches(progress, state.rewardCache, state.modeId);
|
|
}
|
|
return Object.freeze({
|
|
progress,
|
|
changed: JSON.stringify(progress) !== JSON.stringify(original),
|
|
});
|
|
}
|