forked from phenom/healer-man
630 lines
20 KiB
TypeScript
630 lines
20 KiB
TypeScript
import { create } from "zustand";
|
|
import {
|
|
MANASTORM_MAX_RESURRECTION_CHARGES,
|
|
drawManastormStage,
|
|
getManastormCatalog,
|
|
manastormAffixes,
|
|
manastormAvailableLoadoutIds,
|
|
manastormCheckpointForLevel,
|
|
manastormCacheChance,
|
|
manastormGroupModifier,
|
|
isManastormSpecialLevel,
|
|
manastormMessage,
|
|
manastormModeId,
|
|
manastormReward,
|
|
manastormRuntimeScaling,
|
|
normalizeManastormPartySize,
|
|
type ManastormEncounterDefinition,
|
|
type ManastormPartySize,
|
|
type ManastormRewardState,
|
|
type ManastormRewardCacheState,
|
|
type ManastormRunState,
|
|
} from "./manastorm";
|
|
import {
|
|
createManastormClientOverlayState,
|
|
withManastormNextWaveCountdown,
|
|
withManastormSafetyBubble,
|
|
type ManastormClientOverlayState,
|
|
type ManastormSafetyBubbleUpdate,
|
|
} from "./manastormClientOverlay";
|
|
import {
|
|
isManastormPortalCueId,
|
|
manastormCueForStatusTransition,
|
|
type ManastormClientCueRequest,
|
|
type ManastormPortalCueId,
|
|
} from "./manastormClientCues";
|
|
import {
|
|
resolveManastormActiveSpellLoadout,
|
|
validateManastormActiveSpellLoadout,
|
|
type ManastormActiveSpellLoadoutRequest,
|
|
} from "./manastormSpellLoadout";
|
|
|
|
export interface ManastormStartOptions {
|
|
readonly partySize?: number;
|
|
readonly startingLevel?: number;
|
|
readonly unlockedThroughLevel?: number;
|
|
readonly loadoutId?: string | null;
|
|
readonly activeSpellLoadout?: ManastormActiveSpellLoadoutRequest;
|
|
readonly modeId?: number;
|
|
readonly cacheMeter?: number;
|
|
readonly rewardCaches?: readonly ManastormRewardCacheState[];
|
|
readonly random?: () => number;
|
|
}
|
|
|
|
export interface ManastormClientRuntimeFields {
|
|
readonly clientOverlay: ManastormClientOverlayState;
|
|
readonly clientCue: ManastormClientCueRequest | null;
|
|
readonly clientCueRevision: number;
|
|
}
|
|
|
|
export interface ManastormState extends ManastormRunState, ManastormClientRuntimeFields {
|
|
startRun: (
|
|
randomOrOptions?: (() => number) | ManastormStartOptions,
|
|
) => ManastormEncounterDefinition;
|
|
enterStage: () => boolean;
|
|
observeTrash: (cleared: boolean) => void;
|
|
observeBoss: (dead: boolean, looted: boolean, random?: () => number) => void;
|
|
beginTransition: () => boolean;
|
|
prepareNextEncounter: (random?: () => number) => ManastormEncounterDefinition | null;
|
|
finishTransition: () => void;
|
|
useResurrectionCharge: () => boolean;
|
|
restartFromCheckpoint: (random?: () => number) => ManastormEncounterDefinition | null;
|
|
setPartySize: (partySize: number) => boolean;
|
|
setLoadout: (loadoutId: string | null) => boolean;
|
|
setActiveSpellLoadout: (selectedIds: readonly string[]) => boolean;
|
|
openRewardCache: (cacheId: string) => boolean;
|
|
setNextWaveCountdown: (seconds: number | null) => void;
|
|
setSafetyBubble: (update: ManastormSafetyBubbleUpdate) => void;
|
|
requestClientCue: (cueId: ManastormPortalCueId) => boolean;
|
|
clearClientCue: () => void;
|
|
failRun: () => boolean;
|
|
resetRun: () => void;
|
|
}
|
|
|
|
function safeMaximumLevel(): number {
|
|
return Math.max(1, Math.trunc(getManastormCatalog().maximumLevel ?? 16_384));
|
|
}
|
|
|
|
function runtimeFields(
|
|
level: number,
|
|
partySize: ManastormPartySize,
|
|
requestedLoadoutId?: string | null,
|
|
requestedModeId?: number,
|
|
unlockedThroughLevel = level,
|
|
) {
|
|
const catalog = getManastormCatalog();
|
|
const modeId = Math.trunc(requestedModeId ?? manastormModeId(catalog));
|
|
const availableLoadoutIds = manastormAvailableLoadoutIds(level, partySize, catalog);
|
|
const preferredLoadoutId = requestedLoadoutId === undefined
|
|
? catalog.defaultLoadoutId ?? null
|
|
: requestedLoadoutId;
|
|
const loadoutId = preferredLoadoutId && availableLoadoutIds.includes(preferredLoadoutId)
|
|
? preferredLoadoutId
|
|
: availableLoadoutIds[0] ?? null;
|
|
return {
|
|
catalogId: catalog.id ?? "manastorm-catalog",
|
|
modeId,
|
|
groupModifier: manastormGroupModifier(partySize, catalog, modeId),
|
|
scaling: manastormRuntimeScaling(level, partySize, catalog, modeId),
|
|
activeAffixes: manastormAffixes(level, catalog, modeId, unlockedThroughLevel),
|
|
availableLoadoutIds,
|
|
loadoutId,
|
|
message: manastormMessage(level, partySize, catalog, modeId),
|
|
};
|
|
}
|
|
|
|
const initialRunState = (
|
|
partySize: ManastormPartySize = 5,
|
|
transitionRevision = 0,
|
|
): ManastormRunState & ManastormClientRuntimeFields => {
|
|
const level = 1;
|
|
const runtime = runtimeFields(level, partySize);
|
|
return {
|
|
status: "idle",
|
|
phase: "idle",
|
|
level,
|
|
maximumLevel: safeMaximumLevel(),
|
|
unlockedThroughLevel: 1,
|
|
completed: 0,
|
|
checkpointLevel: 1,
|
|
milestone: false,
|
|
partySize,
|
|
maxResurrectionCharges: MANASTORM_MAX_RESURRECTION_CHARGES,
|
|
resurrectionCharges: MANASTORM_MAX_RESURRECTION_CHARGES,
|
|
currentEncounter: null,
|
|
encounterBag: [],
|
|
previousEncounterId: null,
|
|
transitionRevision,
|
|
...runtime,
|
|
pendingReward: null,
|
|
lastReward: null,
|
|
rewardCache: [],
|
|
cacheMeter: 0,
|
|
bonusCacheThreshold: 5,
|
|
currency: {},
|
|
activeSpellLoadout: resolveManastormActiveSpellLoadout(),
|
|
messageRevision: 0,
|
|
clientOverlay: createManastormClientOverlayState(),
|
|
clientCue: null,
|
|
clientCueRevision: 0,
|
|
};
|
|
};
|
|
|
|
function cueFieldsForStatusTransition(
|
|
state: ManastormClientRuntimeFields & Pick<ManastormRunState, "status">,
|
|
nextStatus: string,
|
|
): Partial<ManastormClientRuntimeFields> {
|
|
const cueId = manastormCueForStatusTransition(state.status, nextStatus);
|
|
if (!cueId) return {};
|
|
const revision = state.clientCueRevision + 1;
|
|
return {
|
|
clientCue: { cueId, revision },
|
|
clientCueRevision: revision,
|
|
};
|
|
}
|
|
|
|
function startOptions(
|
|
randomOrOptions?: (() => number) | ManastormStartOptions,
|
|
): Required<Pick<ManastormStartOptions, "partySize" | "startingLevel">>
|
|
& Omit<ManastormStartOptions, "partySize" | "startingLevel"> {
|
|
if (typeof randomOrOptions === "function") {
|
|
return {
|
|
partySize: 5,
|
|
startingLevel: 1,
|
|
random: randomOrOptions,
|
|
};
|
|
}
|
|
return {
|
|
...randomOrOptions,
|
|
partySize: randomOrOptions?.partySize ?? 5,
|
|
startingLevel: randomOrOptions?.startingLevel ?? 1,
|
|
};
|
|
}
|
|
|
|
const MAX_PERSISTED_CACHE_ID_LENGTH = 160;
|
|
|
|
function uniqueCacheInstanceId(
|
|
baseId: string,
|
|
caches: readonly ManastormRewardCacheState[],
|
|
): string {
|
|
const usedIds = new Set(caches.map(({ id }) => id));
|
|
if (!usedIds.has(baseId)) return baseId;
|
|
for (let instance = 2; ; instance += 1) {
|
|
const suffix = `:instance-${instance}`;
|
|
const candidate = `${baseId.slice(0, MAX_PERSISTED_CACHE_ID_LENGTH - suffix.length)}${suffix}`;
|
|
if (!usedIds.has(candidate)) return candidate;
|
|
}
|
|
}
|
|
|
|
function grantedRewardState(
|
|
state: ManastormRunState,
|
|
reward: ManastormRewardState,
|
|
random: () => number,
|
|
): Partial<ManastormRunState> {
|
|
const rewardCache = [...state.rewardCache];
|
|
const cacheChance = reward.cachePolicy === "accumulating-chance"
|
|
? manastormCacheChance(state.level, state.cacheMeter)
|
|
: 0;
|
|
const chanceAwarded = reward.cachePolicy === "accumulating-chance"
|
|
&& random() < cacheChance;
|
|
const cacheAwarded = reward.cachePolicy === "guaranteed" || chanceAwarded;
|
|
const badLuckAwarded = chanceAwarded && state.cacheMeter >= state.bonusCacheThreshold - 1;
|
|
const awardedCache = reward.cacheId && cacheAwarded
|
|
? [
|
|
{
|
|
id: uniqueCacheInstanceId(reward.id, rewardCache),
|
|
cacheId: badLuckAwarded ? "manastorm-bonus-cache" : reward.cacheId,
|
|
rewardId: reward.rewardId,
|
|
stageId: reward.stageId,
|
|
level: reward.level,
|
|
rewardMultiplier: reward.rewardMultiplier,
|
|
},
|
|
]
|
|
: [];
|
|
rewardCache.push(...awardedCache);
|
|
// The meter protects against consecutive cacheless clears. A normal cache
|
|
// satisfies the protection immediately, so it resets the streak without
|
|
// also producing the guaranteed bonus cache.
|
|
const accumulatedMeter = cacheAwarded
|
|
? 0
|
|
: state.cacheMeter + 1;
|
|
const bonusCacheEarned = reward.cachePolicy === "none"
|
|
&& accumulatedMeter >= state.bonusCacheThreshold;
|
|
const bonusCache = bonusCacheEarned
|
|
? [{
|
|
id: uniqueCacheInstanceId(`${reward.id}:bonus-cache`, rewardCache),
|
|
cacheId: "manastorm-bonus-cache",
|
|
rewardId: "bonus-cache-bad-luck-protection",
|
|
stageId: reward.stageId,
|
|
level: reward.level,
|
|
rewardMultiplier: reward.rewardMultiplier,
|
|
}]
|
|
: [];
|
|
rewardCache.push(...bonusCache);
|
|
return {
|
|
pendingReward: null,
|
|
lastReward: reward,
|
|
rewardCache,
|
|
cacheMeter: bonusCacheEarned ? 0 : accumulatedMeter,
|
|
currency: {
|
|
...state.currency,
|
|
[reward.currencyId]: (state.currency[reward.currencyId] ?? 0) + reward.currencyAmount,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const useManastormStore = create<ManastormState>((set, get) => ({
|
|
...initialRunState(),
|
|
|
|
startRun: (randomOrOptions) => {
|
|
const options = startOptions(randomOrOptions);
|
|
const partySize = normalizeManastormPartySize(options.partySize);
|
|
const maximumLevel = safeMaximumLevel();
|
|
const level = Math.max(1, Math.min(maximumLevel, Math.trunc(options.startingLevel)));
|
|
const unlockedThroughLevel = Math.max(
|
|
level,
|
|
Math.min(
|
|
maximumLevel,
|
|
Math.trunc(options.unlockedThroughLevel ?? level),
|
|
),
|
|
);
|
|
const catalog = getManastormCatalog();
|
|
const modeId = Math.trunc(options.modeId ?? manastormModeId(catalog));
|
|
const draw = drawManastormStage(
|
|
level,
|
|
partySize,
|
|
[],
|
|
null,
|
|
options.random ?? Math.random,
|
|
catalog,
|
|
modeId,
|
|
unlockedThroughLevel,
|
|
);
|
|
const runtime = runtimeFields(
|
|
level,
|
|
partySize,
|
|
options.loadoutId,
|
|
modeId,
|
|
unlockedThroughLevel,
|
|
);
|
|
const activeSpellLoadout = resolveManastormActiveSpellLoadout(options.activeSpellLoadout);
|
|
set((state) => ({
|
|
...initialRunState(partySize, state.transitionRevision + 1),
|
|
status: "entering",
|
|
phase: "enter",
|
|
level,
|
|
maximumLevel,
|
|
unlockedThroughLevel,
|
|
completed: level - 1,
|
|
checkpointLevel: manastormCheckpointForLevel(level),
|
|
milestone: isManastormSpecialLevel(level),
|
|
currentEncounter: draw.encounter,
|
|
encounterBag: draw.remainingBag,
|
|
...runtime,
|
|
activeSpellLoadout,
|
|
cacheMeter: Math.max(0, Math.trunc(options.cacheMeter ?? 0)),
|
|
rewardCache: options.rewardCaches
|
|
? Object.freeze(options.rewardCaches.map((cache) => Object.freeze({ ...cache })))
|
|
: Object.freeze([]),
|
|
message: activeSpellLoadout.validationError
|
|
? `Local active-spell selection ignored: ${activeSpellLoadout.validationError}`
|
|
: runtime.message,
|
|
messageRevision: state.messageRevision + 1,
|
|
}));
|
|
return draw.encounter;
|
|
},
|
|
|
|
enterStage: () => {
|
|
const state = get();
|
|
if (state.status !== "entering" || !state.currentEncounter) return false;
|
|
set({
|
|
status: "clearing-trash",
|
|
phase: "trash",
|
|
message: `Break Chaotic Link by clearing guardians, or challenge empowered ${state.currentEncounter.bossName} now.`,
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
observeTrash: (cleared) => set((state) => {
|
|
if (state.status !== "clearing-trash" || !cleared || !state.currentEncounter) return state;
|
|
return {
|
|
status: "fighting",
|
|
phase: "boss",
|
|
message: `Defeat ${state.currentEncounter.bossName}.`,
|
|
messageRevision: state.messageRevision + 1,
|
|
};
|
|
}),
|
|
|
|
observeBoss: (dead, looted, random = Math.random) => set((state) => {
|
|
if (
|
|
state.status !== "fighting"
|
|
&& state.status !== "awaiting-loot"
|
|
) return state;
|
|
if (!dead || !state.currentEncounter) return state;
|
|
const pendingReward = state.pendingReward ?? manastormReward(
|
|
state.currentEncounter,
|
|
state.level,
|
|
state.partySize,
|
|
getManastormCatalog(),
|
|
state.modeId,
|
|
);
|
|
if (!looted) {
|
|
if (state.status === "awaiting-loot" && state.pendingReward === pendingReward) return state;
|
|
return {
|
|
status: "awaiting-loot",
|
|
phase: "loot",
|
|
pendingReward,
|
|
message: `Claim ${state.currentEncounter.bossName}'s reward.`,
|
|
messageRevision: state.messageRevision + 1,
|
|
};
|
|
}
|
|
const completed = Math.max(state.completed, state.level);
|
|
return {
|
|
status: "portal-ready",
|
|
phase: "portal",
|
|
completed,
|
|
checkpointLevel: isManastormSpecialLevel(state.level)
|
|
? state.level + 1
|
|
: state.checkpointLevel,
|
|
...grantedRewardState(state, pendingReward, random),
|
|
...cueFieldsForStatusTransition(state, "portal-ready"),
|
|
message: "The next Manastorm portal is open.",
|
|
messageRevision: state.messageRevision + 1,
|
|
};
|
|
}),
|
|
|
|
beginTransition: () => {
|
|
const state = get();
|
|
if (state.status !== "portal-ready") return false;
|
|
set({
|
|
status: "transitioning",
|
|
phase: "next",
|
|
clientOverlay: createManastormClientOverlayState(),
|
|
message: "The storm is selecting the next stage.",
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
prepareNextEncounter: (random = Math.random) => {
|
|
const state = get();
|
|
if (state.status !== "transitioning" || !state.currentEncounter) return null;
|
|
const completed = Math.max(state.completed, state.level);
|
|
const nextLevel = state.level + 1;
|
|
if (nextLevel > state.maximumLevel) {
|
|
set({
|
|
status: "completed",
|
|
phase: "complete",
|
|
completed,
|
|
previousEncounterId: state.currentEncounter.id,
|
|
currentEncounter: null,
|
|
encounterBag: [],
|
|
clientOverlay: createManastormClientOverlayState(),
|
|
clientCue: null,
|
|
message: "The final Manastorm collapses. Run complete.",
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return null;
|
|
}
|
|
const catalog = getManastormCatalog();
|
|
const modeId = state.modeId;
|
|
const draw = drawManastormStage(
|
|
nextLevel,
|
|
state.partySize,
|
|
state.encounterBag,
|
|
state.currentEncounter.id,
|
|
random,
|
|
catalog,
|
|
modeId,
|
|
Math.max(state.unlockedThroughLevel, nextLevel),
|
|
);
|
|
const nextUnlockedThroughLevel = Math.max(state.unlockedThroughLevel, nextLevel);
|
|
const runtime = runtimeFields(
|
|
nextLevel,
|
|
state.partySize,
|
|
state.loadoutId,
|
|
modeId,
|
|
nextUnlockedThroughLevel,
|
|
);
|
|
set({
|
|
level: nextLevel,
|
|
unlockedThroughLevel: nextUnlockedThroughLevel,
|
|
completed,
|
|
checkpointLevel: manastormCheckpointForLevel(nextLevel),
|
|
milestone: isManastormSpecialLevel(nextLevel),
|
|
currentEncounter: draw.encounter,
|
|
encounterBag: draw.remainingBag,
|
|
previousEncounterId: state.currentEncounter.id,
|
|
transitionRevision: state.transitionRevision + 1,
|
|
pendingReward: null,
|
|
resurrectionCharges: state.maxResurrectionCharges,
|
|
clientOverlay: createManastormClientOverlayState(),
|
|
...runtime,
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return draw.encounter;
|
|
},
|
|
|
|
finishTransition: () => set((state) => state.status === "transitioning"
|
|
? {
|
|
status: "entering",
|
|
phase: "enter",
|
|
message: state.message,
|
|
messageRevision: state.messageRevision + 1,
|
|
}
|
|
: state),
|
|
|
|
useResurrectionCharge: () => {
|
|
const state = get();
|
|
if (
|
|
state.status === "idle"
|
|
|| state.status === "failed"
|
|
|| state.status === "completed"
|
|
|| state.resurrectionCharges <= 0
|
|
) return false;
|
|
set({
|
|
resurrectionCharges: state.resurrectionCharges - 1,
|
|
message: `${state.resurrectionCharges - 1} shared resurrection charge${state.resurrectionCharges - 1 === 1 ? "" : "s"} remaining.`,
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
restartFromCheckpoint: (random = Math.random) => {
|
|
const state = get();
|
|
if (state.status !== "failed") return null;
|
|
const catalog = getManastormCatalog();
|
|
const level = state.checkpointLevel;
|
|
const modeId = state.modeId;
|
|
const draw = drawManastormStage(
|
|
level,
|
|
state.partySize,
|
|
[],
|
|
state.previousEncounterId,
|
|
random,
|
|
catalog,
|
|
modeId,
|
|
state.unlockedThroughLevel,
|
|
);
|
|
const runtime = runtimeFields(
|
|
level,
|
|
state.partySize,
|
|
state.loadoutId,
|
|
modeId,
|
|
state.unlockedThroughLevel,
|
|
);
|
|
set({
|
|
status: "entering",
|
|
phase: "enter",
|
|
level,
|
|
completed: level - 1,
|
|
milestone: isManastormSpecialLevel(level),
|
|
currentEncounter: draw.encounter,
|
|
encounterBag: draw.remainingBag,
|
|
resurrectionCharges: state.maxResurrectionCharges,
|
|
pendingReward: null,
|
|
clientOverlay: createManastormClientOverlayState(),
|
|
clientCue: null,
|
|
transitionRevision: state.transitionRevision + 1,
|
|
...runtime,
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return draw.encounter;
|
|
},
|
|
|
|
setPartySize: (partySize) => {
|
|
const state = get();
|
|
if (state.status !== "idle" && state.status !== "failed" && state.status !== "completed") return false;
|
|
const normalized = normalizeManastormPartySize(partySize);
|
|
const runtime = runtimeFields(
|
|
state.level,
|
|
normalized,
|
|
state.loadoutId,
|
|
state.modeId,
|
|
state.unlockedThroughLevel,
|
|
);
|
|
set({
|
|
partySize: normalized,
|
|
...runtime,
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
setLoadout: (loadoutId) => {
|
|
const state = get();
|
|
if (loadoutId !== null && !state.availableLoadoutIds.includes(loadoutId)) return false;
|
|
set({
|
|
loadoutId,
|
|
message: loadoutId ? `Loadout selected: ${loadoutId}.` : "No Manastorm loadout selected.",
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
setActiveSpellLoadout: (selectedIds) => {
|
|
const state = get();
|
|
const validation = validateManastormActiveSpellLoadout(
|
|
selectedIds,
|
|
state.activeSpellLoadout.available,
|
|
state.activeSpellLoadout.maxSelections,
|
|
);
|
|
if (!validation.ok) return false;
|
|
set({
|
|
activeSpellLoadout: {
|
|
...state.activeSpellLoadout,
|
|
selectedIds: validation.selectedIds,
|
|
selected: validation.selected,
|
|
validationError: null,
|
|
},
|
|
message: validation.selectedIds.length
|
|
? `${validation.selectedIds.length} local active-spell selection${validation.selectedIds.length === 1 ? "" : "s"} prepared.`
|
|
: "No local active-spell selections prepared.",
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
openRewardCache: (cacheId) => {
|
|
const state = get();
|
|
const cacheIndex = state.rewardCache.findIndex((candidate) => candidate.id === cacheId);
|
|
const cache = state.rewardCache[cacheIndex];
|
|
if (!cache) return false;
|
|
const multiplier = Math.max(
|
|
0,
|
|
Number.isFinite(cache.rewardMultiplier) ? cache.rewardMultiplier ?? 1 : 1,
|
|
);
|
|
set({
|
|
rewardCache: state.rewardCache.filter((_, index) => index !== cacheIndex),
|
|
currency: {
|
|
...state.currency,
|
|
bedlamBullions: (state.currency.bedlamBullions ?? 0) + Math.max(1, Math.round(multiplier)),
|
|
bonzoBolts: (state.currency.bonzoBolts ?? 0)
|
|
+ Math.max(1, Math.round(Math.ceil(cache.level / 5) * multiplier)),
|
|
},
|
|
message: "Manastorm cache opened.",
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
setNextWaveCountdown: (seconds) => set((state) => ({
|
|
clientOverlay: withManastormNextWaveCountdown(state.clientOverlay, seconds),
|
|
})),
|
|
|
|
setSafetyBubble: (update) => set((state) => ({
|
|
clientOverlay: withManastormSafetyBubble(state.clientOverlay, update),
|
|
})),
|
|
|
|
requestClientCue: (cueId) => {
|
|
if (!isManastormPortalCueId(cueId)) return false;
|
|
set((state) => {
|
|
const revision = state.clientCueRevision + 1;
|
|
return {
|
|
clientCue: { cueId, revision },
|
|
clientCueRevision: revision,
|
|
};
|
|
});
|
|
return true;
|
|
},
|
|
|
|
clearClientCue: () => set({ clientCue: null }),
|
|
|
|
failRun: () => {
|
|
const state = get();
|
|
if (state.status === "idle" || state.status === "failed" || state.status === "completed") return false;
|
|
set({
|
|
status: "failed",
|
|
phase: "failed",
|
|
clientOverlay: createManastormClientOverlayState(),
|
|
clientCue: null,
|
|
message: `Run failed. Checkpoint ${state.checkpointLevel} is available.`,
|
|
messageRevision: state.messageRevision + 1,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
resetRun: () => set((state) => initialRunState(state.partySize, state.transitionRevision + 1)),
|
|
}));
|