This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -1,9 +1,3 @@
import type {
RuntimeStoryActionRequest,
RuntimeStoryActionResponse,
RuntimeStoryOptionView,
RuntimeStoryStateRequest,
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryState';
import type {
RuntimeStoryChoicePayload,
ServerRuntimeFunctionId,
@@ -13,6 +7,13 @@ import {
SERVER_RUNTIME_FUNCTION_IDS,
TASK5_RUNTIME_FUNCTION_IDS,
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryAction';
import type {
RuntimeStoryActionRequest,
RuntimeStoryActionResponse,
RuntimeStoryBootstrapRequest,
RuntimeStoryBootstrapResponse,
RuntimeStoryOptionView,
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryState';
import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { GameState, StoryMoment, StoryOption } from '../../types';
@@ -44,11 +45,13 @@ export type RuntimeStoryResponse = RuntimeStoryActionResponse<
GameState,
StoryMoment
>;
export type { RuntimeStoryChoicePayload };
export type RuntimeStorySnapshotRequest = RuntimeStoryStateRequest<
export type RuntimeStoryBootstrapResult = RuntimeStoryBootstrapResponse<
GameState,
StoryMoment
>['snapshot'];
>;
export type RuntimeStoryInventoryView =
RuntimeStoryResponse['viewModel']['inventory'];
export type { RuntimeStoryChoicePayload };
function requestRuntimeStoryJson<T>(
path: string,
@@ -56,6 +59,8 @@ function requestRuntimeStoryJson<T>(
fallbackMessage: string,
options: RpgRuntimeStoryClientOptions = {},
) {
// 中文注释runtime story 请求默认带一层轻量重试,
// 因为这里既有 state 拉取,也有动作结算,请求失败会直接影响当前回合体验。
return requestJson<T>(
`${RUNTIME_STORY_API_BASE}${path}`,
{
@@ -71,6 +76,8 @@ function createRuntimeStoryOption(
option: RuntimeStoryOptionView,
_gameState?: Pick<GameState, 'currentEncounter'>,
): StoryOption {
// 中文注释:服务端 viewModel 当前只返回动作层字段,
// 前端在这里补齐 StoryOption 所需的基础表现字段,保持冒险面板消费接口稳定。
return {
functionId: option.functionId,
actionText: option.actionText,
@@ -118,6 +125,8 @@ export function isServerRuntimeFunctionId(
}
export function shouldUseServerRuntimeOptions(options: StoryOption[] | null) {
// 中文注释:只有当整组选项都已经切换到服务端 function id 体系时,
// 前端才把这轮视为“纯服务端 runtime 选项”,避免本地/服务端动作混用。
return Boolean(
options?.length &&
options.every((option) => isServerRuntimeFunctionId(option.functionId)),
@@ -152,6 +161,8 @@ export function resolveRuntimeStoryMoment(params: {
fallbackGameState?: Pick<GameState, 'currentEncounter'>;
fallbackStoryText?: string;
}) {
// 中文注释:对话态 story 往往包含 deferredOptions / dialogue 结构,
// 这类内容如果已经存进快照,应优先使用快照,避免被普通 presentation 选项覆盖。
if (shouldPreferSnapshotStory(params.hydratedSnapshot.currentStory)) {
return params.hydratedSnapshot.currentStory!;
}
@@ -178,32 +189,18 @@ export async function getRuntimeStoryState(
params: {
sessionId: string;
clientVersion?: number;
snapshot?: RuntimeStorySnapshotRequest;
},
options: RpgRuntimeStoryClientOptions = {},
) {
const normalizedSessionId = params.sessionId || DEFAULT_SESSION_ID;
const response = params.snapshot
? await requestRuntimeStoryJson<RuntimeStoryResponse>(
'/state/resolve',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: normalizedSessionId,
clientVersion: params.clientVersion,
snapshot: params.snapshot,
} satisfies RuntimeStoryStateRequest<GameState, StoryMoment>),
},
'读取运行时故事状态失败',
options,
)
: await requestRuntimeStoryJson<RuntimeStoryResponse>(
`/state/${encodeURIComponent(normalizedSessionId)}`,
{ method: 'GET' },
'读取运行时故事状态失败',
options,
);
// 中文注释runtime story 状态读取只按服务端持久化 sessionId 拉取,
// 不再允许前端上传本地 GameState 快照参与解析。
const response = await requestRuntimeStoryJson<RuntimeStoryResponse>(
`/state/${encodeURIComponent(normalizedSessionId)}`,
{ method: 'GET' },
'读取运行时故事状态失败',
options,
);
return {
...response,
@@ -213,6 +210,51 @@ export async function getRuntimeStoryState(
} satisfies RuntimeStoryResponse;
}
export async function loadRuntimeInventoryView(
params: {
gameState: Pick<GameState, 'runtimeSessionId' | 'runtimeActionVersion'>;
},
options: RpgRuntimeStoryClientOptions = {},
) {
// 中文注释:背包 / 装备 / 锻造 view 只读取后端已持久化的 runtime session
// 前端不再用本地背包、货币或装备状态重算配方可用性。
const response = await getRuntimeStoryState(
{
sessionId: getRuntimeSessionId(params.gameState),
clientVersion: getRuntimeClientVersion(params.gameState),
},
options,
);
return response.viewModel.inventory;
}
export async function beginRuntimeStorySession(
params: RuntimeStoryBootstrapRequest<
GameState['customWorldProfile'],
NonNullable<GameState['playerCharacter']>
>,
options: RpgRuntimeStoryClientOptions = {},
) {
const response = await requestRuntimeStoryJson<RuntimeStoryBootstrapResult>(
'/sessions',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
},
'初始化运行时开局失败',
options,
);
return {
...response,
snapshot: rehydrateSavedSnapshot(
response.snapshot as HydratedSavedGameSnapshot,
),
} satisfies RuntimeStoryBootstrapResult;
}
export async function resolveRuntimeStoryAction(
params: {
sessionId?: string;
@@ -220,10 +262,11 @@ export async function resolveRuntimeStoryAction(
option: Pick<StoryOption, 'functionId' | 'actionText'>;
targetId?: string;
payload?: RuntimeStoryChoicePayload;
snapshot?: RuntimeStorySnapshotRequest;
},
options: RpgRuntimeStoryClientOptions = {},
) {
// 中文注释story_choice 是当前前端统一提交给服务端的动作包裹格式,
// optionText 会一起带上,方便服务端日志、提示词和调试链查看用户当轮选择。
const response = await requestRuntimeStoryJson<RuntimeStoryResponse>(
'/actions/resolve',
{
@@ -241,7 +284,6 @@ export async function resolveRuntimeStoryAction(
...(params.payload ?? {}),
},
},
snapshot: params.snapshot,
} satisfies RuntimeStoryActionRequest),
},
'执行运行时动作失败',
@@ -260,19 +302,23 @@ export function getRuntimeActionSnapshot(response: RuntimeStoryResponse) {
return rehydrateSavedSnapshot(response.snapshot as HydratedSavedGameSnapshot);
}
export const beginRpgRuntimeStorySession = beginRuntimeStorySession;
export const getRpgRuntimeActionSnapshot = getRuntimeActionSnapshot;
export const getRpgRuntimeClientVersion = getRuntimeClientVersion;
export const getRpgRuntimeSessionId = getRuntimeSessionId;
export const getRpgRuntimeStoryState = getRuntimeStoryState;
export const isRpgRuntimeServerFunctionId = isServerRuntimeFunctionId;
export const isRpgRuntimeTaskFunctionId = isTask5RuntimeFunctionId;
export const loadRpgRuntimeInventoryView = loadRuntimeInventoryView;
export const resolveRpgRuntimeStoryAction = resolveRuntimeStoryAction;
export const resolveRpgRuntimeStoryMoment = resolveRuntimeStoryMoment;
export const shouldUseRpgRuntimeServerOptions = shouldUseServerRuntimeOptions;
export const rpgRuntimeStoryClient = {
beginSession: beginRpgRuntimeStorySession,
getActionSnapshot: getRpgRuntimeActionSnapshot,
getClientVersion: getRpgRuntimeClientVersion,
getInventoryView: loadRpgRuntimeInventoryView,
getSessionId: getRpgRuntimeSessionId,
getState: getRpgRuntimeStoryState,
resolveAction: resolveRpgRuntimeStoryAction,