import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot'; import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes'; import { getRpgRuntimeClientVersion, getRpgRuntimeSessionId, getRpgRuntimeStoryState, resolveRpgRuntimeStoryAction, type RuntimeStorySnapshotRequest, resolveRpgRuntimeStoryMoment, type RuntimeStoryChoicePayload, type RuntimeStoryResponse, } from '../../services/rpg-runtime/rpgRuntimeStoryClient'; import type { GameState, StoryMoment, StoryOption } from '../../types'; function getRuntimeResponseOptions(response: RuntimeStoryResponse) { return response.viewModel.availableOptions.length > 0 ? response.viewModel.availableOptions : response.presentation.options; } function buildRuntimeSnapshotRequest( gameState: GameState, currentStory: StoryMoment | null, ): RuntimeStorySnapshotRequest { return { gameState, bottomTab: 'adventure', currentStory, }; } /** * 前端访问服务端 runtime story 的统一网关。 * 统一处理 option catalog 拉取、继续游戏恢复与正式动作结算。 */ export async function loadServerRuntimeOptionCatalog(params: { gameState: GameState; currentStory: StoryMoment | null; }) { const response = await getRpgRuntimeStoryState({ sessionId: getRpgRuntimeSessionId(params.gameState), clientVersion: getRpgRuntimeClientVersion(params.gameState), snapshot: buildRuntimeSnapshotRequest(params.gameState, params.currentStory), }); const options = resolveRpgRuntimeStoryMoment({ response, hydratedSnapshot: response.snapshot, fallbackGameState: params.gameState, fallbackStoryText: response.presentation.storyText, }).options; return options.length > 0 ? options : null; } export async function resumeServerRuntimeStory( snapshot: HydratedSavedGameSnapshot, ) { const hydratedSnapshot = rehydrateSavedSnapshot(snapshot); const shouldRefreshFromServer = hydratedSnapshot.gameState.currentScene === 'Story' && Boolean(hydratedSnapshot.gameState.worldType) && Boolean(hydratedSnapshot.gameState.playerCharacter); if (!shouldRefreshFromServer) { return { hydratedSnapshot, nextStory: hydratedSnapshot.currentStory, }; } const response = await getRpgRuntimeStoryState({ sessionId: getRpgRuntimeSessionId(hydratedSnapshot.gameState), }); const resumedSnapshot = rehydrateSavedSnapshot(response.snapshot); const runtimeOptions = getRuntimeResponseOptions(response); const nextStory = response.presentation.storyText || runtimeOptions.length > 0 ? resolveRpgRuntimeStoryMoment({ response, hydratedSnapshot: resumedSnapshot, fallbackGameState: hydratedSnapshot.gameState, fallbackStoryText: response.presentation.storyText || resumedSnapshot.currentStory?.text || hydratedSnapshot.currentStory?.text || '', }) : resumedSnapshot.currentStory; return { hydratedSnapshot: resumedSnapshot, nextStory, }; } export async function resolveServerRuntimeChoice(params: { gameState: GameState; currentStory: StoryMoment | null; option: Pick & Partial>; payload?: RuntimeStoryChoicePayload; }) { const response = await resolveRpgRuntimeStoryAction({ sessionId: getRpgRuntimeSessionId(params.gameState), clientVersion: getRpgRuntimeClientVersion(params.gameState), option: params.option, targetId: params.option.interaction?.kind === 'npc' ? params.option.interaction.npcId : undefined, payload: params.payload, snapshot: buildRuntimeSnapshotRequest(params.gameState, params.currentStory), }); const hydratedSnapshot = rehydrateSavedSnapshot(response.snapshot); return { response, hydratedSnapshot, nextStory: resolveRpgRuntimeStoryMoment({ response, hydratedSnapshot, fallbackGameState: params.gameState, fallbackStoryText: response.presentation.storyText || hydratedSnapshot.currentStory?.text || params.option.actionText, }), }; } export type LoadRpgRuntimeOptionCatalogParams = Parameters< typeof loadServerRuntimeOptionCatalog >[0]; export type ResolveRpgRuntimeChoiceParams = Parameters< typeof resolveServerRuntimeChoice >[0]; export const loadRpgRuntimeOptionCatalog = loadServerRuntimeOptionCatalog; export const resumeRpgRuntimeStory = resumeServerRuntimeStory; export const resolveRpgRuntimeChoice = resolveServerRuntimeChoice;