Files
Genarrative/src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts
2026-04-21 18:27:46 +08:00

141 lines
4.5 KiB
TypeScript

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<StoryOption, 'functionId' | 'actionText'> &
Partial<Pick<StoryOption, 'interaction'>>;
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;