Integrate unfinished server-rs refactor worklists
This commit is contained in:
@@ -14,6 +14,14 @@ import type {
|
||||
RuntimeStoryBootstrapResponse,
|
||||
RuntimeStoryOptionView,
|
||||
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryState';
|
||||
import type {
|
||||
BeginStorySessionRequest,
|
||||
ContinueStoryRequest,
|
||||
StorySessionMutationResponse,
|
||||
StorySessionStateResponse,
|
||||
StoryRuntimeOptionProjection,
|
||||
StoryRuntimeProjectionResponse,
|
||||
} from '../../../packages/shared/src/contracts/story';
|
||||
import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import type { GameState, StoryMoment, StoryOption } from '../../types';
|
||||
@@ -21,6 +29,7 @@ import { AnimationState } from '../../types';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const RUNTIME_STORY_API_BASE = '/api/runtime/story';
|
||||
const STORY_SESSIONS_API_BASE = '/api/story/sessions';
|
||||
const DEFAULT_SESSION_ID = 'runtime-main';
|
||||
const RUNTIME_STORY_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
@@ -49,6 +58,9 @@ export type RuntimeStoryBootstrapResult = RuntimeStoryBootstrapResponse<
|
||||
GameState,
|
||||
StoryMoment
|
||||
>;
|
||||
export type StorySessionMutationResult = StorySessionMutationResponse;
|
||||
export type StorySessionStateResult = StorySessionStateResponse;
|
||||
export type RuntimeStoryProjectionResult = StoryRuntimeProjectionResponse;
|
||||
export type RuntimeStoryInventoryView =
|
||||
RuntimeStoryResponse['viewModel']['inventory'];
|
||||
export type { RuntimeStoryChoicePayload };
|
||||
@@ -72,6 +84,23 @@ function requestRuntimeStoryJson<T>(
|
||||
);
|
||||
}
|
||||
|
||||
function requestStorySessionJson<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
fallbackMessage: string,
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
return requestJson<T>(
|
||||
`${STORY_SESSIONS_API_BASE}${path}`,
|
||||
{
|
||||
...init,
|
||||
signal: options.signal,
|
||||
},
|
||||
fallbackMessage,
|
||||
{ retry: options.retry ?? RUNTIME_STORY_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
function createRuntimeStoryOption(
|
||||
option: RuntimeStoryOptionView,
|
||||
_gameState?: Pick<GameState, 'currentEncounter'>,
|
||||
@@ -98,12 +127,84 @@ function createRuntimeStoryOption(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeProjectionOptionScope(
|
||||
scope: string,
|
||||
): RuntimeStoryOptionView['scope'] {
|
||||
return scope === 'combat' || scope === 'npc' ? scope : 'story';
|
||||
}
|
||||
|
||||
function mapRuntimeProjectionOption(
|
||||
option: StoryRuntimeOptionProjection,
|
||||
): RuntimeStoryOptionView {
|
||||
return {
|
||||
functionId: option.functionId,
|
||||
actionText: option.actionText,
|
||||
detailText: option.detailText ?? undefined,
|
||||
scope: normalizeProjectionOptionScope(option.scope),
|
||||
payload: option.payload ?? undefined,
|
||||
disabled: option.enabled ? undefined : true,
|
||||
reason: option.enabled ? undefined : (option.reason ?? undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function mapRuntimeProjectionInventory(
|
||||
projection: StoryRuntimeProjectionResponse,
|
||||
): RuntimeStoryInventoryView {
|
||||
return {
|
||||
playerCurrency: projection.actor.currency,
|
||||
currencyText: projection.actor.currencyText,
|
||||
inBattle: projection.status.inBattle,
|
||||
backpackItems:
|
||||
projection.inventory
|
||||
.backpackItems as RuntimeStoryInventoryView['backpackItems'],
|
||||
equipmentSlots:
|
||||
projection.inventory
|
||||
.equipmentSlots as RuntimeStoryInventoryView['equipmentSlots'],
|
||||
forgeRecipes:
|
||||
projection.inventory
|
||||
.forgeRecipes as RuntimeStoryInventoryView['forgeRecipes'],
|
||||
};
|
||||
}
|
||||
|
||||
function getRuntimeProjectionStoryText(
|
||||
projection: Pick<
|
||||
StoryRuntimeProjectionResponse,
|
||||
'currentNarrativeText' | 'storySession'
|
||||
>,
|
||||
) {
|
||||
return (
|
||||
projection.currentNarrativeText?.trim() ||
|
||||
projection.storySession.latestNarrativeText.trim()
|
||||
);
|
||||
}
|
||||
|
||||
export function getRuntimeSessionId(
|
||||
gameState: Pick<GameState, 'runtimeSessionId'>,
|
||||
) {
|
||||
return gameState.runtimeSessionId?.trim() || DEFAULT_SESSION_ID;
|
||||
}
|
||||
|
||||
export function getRuntimeStorySessionId(
|
||||
gameState: Pick<GameState, 'storySessionId'>,
|
||||
) {
|
||||
return normalizeStorySessionId(
|
||||
gameState.storySessionId,
|
||||
'运行时故事会话不存在,无法读取服务端投影',
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeStorySessionId(
|
||||
storySessionId: string | null | undefined,
|
||||
message: string,
|
||||
) {
|
||||
const normalizedStorySessionId = storySessionId?.trim();
|
||||
if (!normalizedStorySessionId) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return normalizedStorySessionId;
|
||||
}
|
||||
|
||||
export function getRuntimeClientVersion(
|
||||
gameState: Pick<GameState, 'runtimeActionVersion'>,
|
||||
) {
|
||||
@@ -146,6 +247,17 @@ export function buildStoryMomentFromRuntimeOptions(params: {
|
||||
} satisfies StoryMoment;
|
||||
}
|
||||
|
||||
export function buildStoryMomentFromRuntimeProjection(params: {
|
||||
projection: StoryRuntimeProjectionResponse;
|
||||
gameState?: Pick<GameState, 'currentEncounter'>;
|
||||
}): StoryMoment {
|
||||
return buildStoryMomentFromRuntimeOptions({
|
||||
storyText: getRuntimeProjectionStoryText(params.projection),
|
||||
options: params.projection.options.map(mapRuntimeProjectionOption),
|
||||
gameState: params.gameState,
|
||||
});
|
||||
}
|
||||
|
||||
function shouldPreferSnapshotStory(story: StoryMoment | null) {
|
||||
return Boolean(
|
||||
story &&
|
||||
@@ -185,48 +297,114 @@ export function resolveRuntimeStoryMoment(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRuntimeStoryState(
|
||||
export async function beginStorySession(
|
||||
params: BeginStorySessionRequest,
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
return requestStorySessionJson<StorySessionMutationResult>(
|
||||
'',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params),
|
||||
},
|
||||
'创建故事会话失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function continueStorySession(
|
||||
params: ContinueStoryRequest,
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
const storySessionId = normalizeStorySessionId(
|
||||
params.storySessionId,
|
||||
'故事会话不存在,无法继续故事',
|
||||
);
|
||||
|
||||
return requestStorySessionJson<StorySessionMutationResult>(
|
||||
'/continue',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
storySessionId,
|
||||
}),
|
||||
},
|
||||
'继续故事会话失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStorySessionState(
|
||||
params: { storySessionId: string },
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
const storySessionId = normalizeStorySessionId(
|
||||
params.storySessionId,
|
||||
'故事会话不存在,无法读取故事会话状态',
|
||||
);
|
||||
|
||||
return requestStorySessionJson<StorySessionStateResult>(
|
||||
`/${encodeURIComponent(storySessionId)}/state`,
|
||||
{ method: 'GET' },
|
||||
'读取故事会话状态失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStoryRuntimeProjection(
|
||||
params: {
|
||||
sessionId: string;
|
||||
storySessionId: string;
|
||||
clientVersion?: number;
|
||||
},
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
const normalizedSessionId = params.sessionId || DEFAULT_SESSION_ID;
|
||||
// 中文注释:runtime story 状态读取只按服务端持久化 sessionId 拉取,
|
||||
// 不再允许前端上传本地 GameState 快照参与解析。
|
||||
const response = await requestRuntimeStoryJson<RuntimeStoryResponse>(
|
||||
`/state/${encodeURIComponent(normalizedSessionId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取运行时故事状态失败',
|
||||
options,
|
||||
const storySessionId = normalizeStorySessionId(
|
||||
params.storySessionId,
|
||||
'运行时故事会话不存在,无法读取服务端投影',
|
||||
);
|
||||
|
||||
return {
|
||||
...response,
|
||||
snapshot: rehydrateSavedSnapshot(
|
||||
response.snapshot as HydratedSavedGameSnapshot,
|
||||
),
|
||||
} satisfies RuntimeStoryResponse;
|
||||
// 中文注释:当前 BFF route 以 storySessionId 为唯一读取键;
|
||||
// clientVersion 保留在调用签名里,等待后端增量投影契约稳定后再接查询参数。
|
||||
return requestStorySessionJson<RuntimeStoryProjectionResult>(
|
||||
`/${encodeURIComponent(storySessionId)}/runtime-projection`,
|
||||
{ method: 'GET' },
|
||||
'读取运行时故事投影失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRuntimeStoryState(
|
||||
params: {
|
||||
storySessionId: string;
|
||||
clientVersion?: number;
|
||||
},
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
// 中文注释:读取侧正式切到 story session scoped 投影;
|
||||
// 这里不允许用 runtimeSessionId 兜底,避免两个会话主键被悄悄混用。
|
||||
return getStoryRuntimeProjection(params, options);
|
||||
}
|
||||
|
||||
export async function loadRuntimeInventoryView(
|
||||
params: {
|
||||
gameState: Pick<GameState, 'runtimeSessionId' | 'runtimeActionVersion'>;
|
||||
gameState: Pick<GameState, 'storySessionId' | 'runtimeActionVersion'>;
|
||||
},
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
// 中文注释:背包 / 装备 / 锻造 view 只读取后端已持久化的 runtime session;
|
||||
// 中文注释:背包 / 装备 / 锻造 view 只读取 story runtime 投影;
|
||||
// 前端不再用本地背包、货币或装备状态重算配方可用性。
|
||||
const response = await getRuntimeStoryState(
|
||||
{
|
||||
sessionId: getRuntimeSessionId(params.gameState),
|
||||
storySessionId: getRuntimeStorySessionId(params.gameState),
|
||||
clientVersion: getRuntimeClientVersion(params.gameState),
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
return response.viewModel.inventory;
|
||||
return mapRuntimeProjectionInventory(response);
|
||||
}
|
||||
|
||||
export async function beginRuntimeStorySession(
|
||||
@@ -303,25 +481,38 @@ export function getRuntimeActionSnapshot(response: RuntimeStoryResponse) {
|
||||
}
|
||||
|
||||
export const beginRpgRuntimeStorySession = beginRuntimeStorySession;
|
||||
export const beginRpgStorySession = beginStorySession;
|
||||
export const continueRpgStorySession = continueStorySession;
|
||||
export const getRpgStoryRuntimeProjection = getStoryRuntimeProjection;
|
||||
export const getRpgStorySessionState = getStorySessionState;
|
||||
export const getRpgRuntimeActionSnapshot = getRuntimeActionSnapshot;
|
||||
export const getRpgRuntimeClientVersion = getRuntimeClientVersion;
|
||||
export const getRpgRuntimeSessionId = getRuntimeSessionId;
|
||||
export const getRpgRuntimeStorySessionId = getRuntimeStorySessionId;
|
||||
export const getRpgRuntimeStoryState = getRuntimeStoryState;
|
||||
export const isRpgRuntimeServerFunctionId = isServerRuntimeFunctionId;
|
||||
export const isRpgRuntimeTaskFunctionId = isTask5RuntimeFunctionId;
|
||||
export const loadRpgRuntimeInventoryView = loadRuntimeInventoryView;
|
||||
export const resolveRpgRuntimeStoryAction = resolveRuntimeStoryAction;
|
||||
export const resolveRpgRuntimeStoryProjectionMoment =
|
||||
buildStoryMomentFromRuntimeProjection;
|
||||
export const resolveRpgRuntimeStoryMoment = resolveRuntimeStoryMoment;
|
||||
export const shouldUseRpgRuntimeServerOptions = shouldUseServerRuntimeOptions;
|
||||
|
||||
export const rpgRuntimeStoryClient = {
|
||||
beginSession: beginRpgRuntimeStorySession,
|
||||
beginStorySession: beginRpgStorySession,
|
||||
continueStorySession: continueRpgStorySession,
|
||||
getActionSnapshot: getRpgRuntimeActionSnapshot,
|
||||
getClientVersion: getRpgRuntimeClientVersion,
|
||||
getInventoryView: loadRpgRuntimeInventoryView,
|
||||
getSessionId: getRpgRuntimeSessionId,
|
||||
getStoryRuntimeProjection: getRpgStoryRuntimeProjection,
|
||||
getStorySessionId: getRpgRuntimeStorySessionId,
|
||||
getStorySessionState: getRpgStorySessionState,
|
||||
getState: getRpgRuntimeStoryState,
|
||||
resolveAction: resolveRpgRuntimeStoryAction,
|
||||
resolveProjectionMoment: resolveRpgRuntimeStoryProjectionMoment,
|
||||
resolveMoment: resolveRpgRuntimeStoryMoment,
|
||||
shouldUseServerOptions: shouldUseRpgRuntimeServerOptions,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user