Close DDD refactor and remove generated asset proxy
This commit is contained in:
@@ -8,15 +8,16 @@ import {
|
||||
TASK5_RUNTIME_FUNCTION_IDS,
|
||||
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryAction';
|
||||
import type {
|
||||
RuntimeStoryActionRequest,
|
||||
RuntimeStoryActionResponse,
|
||||
RuntimeStoryBootstrapRequest,
|
||||
RuntimeStoryBootstrapResponse,
|
||||
RuntimeStoryOptionView,
|
||||
RuntimeBattlePresentation,
|
||||
RuntimeStoryInventoryViewModel,
|
||||
} from '../../../packages/shared/src/contracts/rpgRuntimeStoryState';
|
||||
import type {
|
||||
BeginStoryRuntimeSessionRequest,
|
||||
BeginStorySessionRequest,
|
||||
ContinueStoryRequest,
|
||||
ResolveStoryRuntimeActionRequest,
|
||||
StoryRuntimeMutationResponse,
|
||||
StorySessionMutationResponse,
|
||||
StorySessionStateResponse,
|
||||
StoryRuntimeOptionProjection,
|
||||
@@ -28,7 +29,6 @@ import type { GameState, StoryMoment, StoryOption } from '../../types';
|
||||
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 = {
|
||||
@@ -50,40 +50,25 @@ export type RpgRuntimeStoryClientOptions = {
|
||||
retry?: ApiRetryOptions;
|
||||
};
|
||||
|
||||
export type RuntimeStoryResponse = RuntimeStoryActionResponse<
|
||||
GameState,
|
||||
StoryMoment
|
||||
>;
|
||||
export type RuntimeStoryBootstrapResult = RuntimeStoryBootstrapResponse<
|
||||
GameState,
|
||||
StoryMoment
|
||||
>;
|
||||
export type RuntimeStoryActionPresentation = {
|
||||
battle?: RuntimeBattlePresentation | null;
|
||||
resultText?: string;
|
||||
storyText?: string;
|
||||
};
|
||||
export type RuntimeStoryInventoryView = RuntimeStoryInventoryViewModel;
|
||||
export type RuntimeStoryResponse = {
|
||||
sessionId: string;
|
||||
serverVersion: number;
|
||||
projection: StoryRuntimeProjectionResponse;
|
||||
snapshot: HydratedSavedGameSnapshot;
|
||||
inventoryView: RuntimeStoryInventoryView;
|
||||
presentation?: RuntimeStoryActionPresentation;
|
||||
};
|
||||
export type StorySessionMutationResult = StorySessionMutationResponse;
|
||||
export type StorySessionStateResult = StorySessionStateResponse;
|
||||
export type RuntimeStoryProjectionResult = StoryRuntimeProjectionResponse;
|
||||
export type RuntimeStoryInventoryView =
|
||||
RuntimeStoryResponse['viewModel']['inventory'];
|
||||
export type { RuntimeStoryChoicePayload };
|
||||
|
||||
function requestRuntimeStoryJson<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
fallbackMessage: string,
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
// 中文注释:runtime story 请求默认带一层轻量重试,
|
||||
// 因为这里既有 state 拉取,也有动作结算,请求失败会直接影响当前回合体验。
|
||||
return requestJson<T>(
|
||||
`${RUNTIME_STORY_API_BASE}${path}`,
|
||||
{
|
||||
...init,
|
||||
signal: options.signal,
|
||||
},
|
||||
fallbackMessage,
|
||||
{ retry: options.retry ?? RUNTIME_STORY_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
function requestStorySessionJson<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
@@ -105,7 +90,7 @@ function createRuntimeStoryOption(
|
||||
option: RuntimeStoryOptionView,
|
||||
_gameState?: Pick<GameState, 'currentEncounter'>,
|
||||
): StoryOption {
|
||||
// 中文注释:服务端 viewModel 当前只返回动作层字段,
|
||||
// 中文注释:服务端投影当前只返回动作层字段,
|
||||
// 前端在这里补齐 StoryOption 所需的基础表现字段,保持冒险面板消费接口稳定。
|
||||
return {
|
||||
functionId: option.functionId,
|
||||
@@ -178,6 +163,47 @@ function getRuntimeProjectionStoryText(
|
||||
);
|
||||
}
|
||||
|
||||
export function buildRuntimeSnapshotFromProjection(
|
||||
projection: StoryRuntimeProjectionResponse,
|
||||
bottomTab: HydratedSavedGameSnapshot['bottomTab'] = 'adventure',
|
||||
): HydratedSavedGameSnapshot {
|
||||
const gameState = {
|
||||
...(projection.gameState as unknown as GameState),
|
||||
runtimeSessionId: projection.storySession.runtimeSessionId,
|
||||
storySessionId: projection.storySession.storySessionId,
|
||||
runtimeActionVersion: projection.serverVersion,
|
||||
} satisfies GameState;
|
||||
const currentStory = buildStoryMomentFromRuntimeProjection({
|
||||
projection,
|
||||
gameState,
|
||||
});
|
||||
|
||||
// 中文注释:新写入接口只返回 story runtime 投影,前端在边界层
|
||||
// 还原为已有运行时快照格式,避免下游 hooks 继续认识旧 action response。
|
||||
return rehydrateSavedSnapshot({
|
||||
version: projection.serverVersion,
|
||||
savedAt: projection.storySession.updatedAt,
|
||||
bottomTab,
|
||||
gameState,
|
||||
currentStory,
|
||||
} as HydratedSavedGameSnapshot);
|
||||
}
|
||||
|
||||
function normalizeRuntimeMutationResponse(
|
||||
response: StoryRuntimeMutationResponse,
|
||||
): RuntimeStoryResponse {
|
||||
const { projection } = response;
|
||||
const snapshot = buildRuntimeSnapshotFromProjection(projection);
|
||||
|
||||
return {
|
||||
sessionId: projection.storySession.runtimeSessionId,
|
||||
serverVersion: projection.serverVersion,
|
||||
projection,
|
||||
snapshot,
|
||||
inventoryView: mapRuntimeProjectionInventory(projection),
|
||||
};
|
||||
}
|
||||
|
||||
export function getRuntimeSessionId(
|
||||
gameState: Pick<GameState, 'runtimeSessionId'>,
|
||||
) {
|
||||
@@ -279,18 +305,13 @@ export function resolveRuntimeStoryMoment(params: {
|
||||
return params.hydratedSnapshot.currentStory!;
|
||||
}
|
||||
|
||||
const options =
|
||||
params.response.viewModel.availableOptions.length > 0
|
||||
? params.response.viewModel.availableOptions
|
||||
: params.response.presentation.options;
|
||||
|
||||
return buildStoryMomentFromRuntimeOptions({
|
||||
storyText:
|
||||
params.response.presentation.storyText ||
|
||||
params.response.presentation?.storyText ||
|
||||
params.hydratedSnapshot.currentStory?.text ||
|
||||
params.fallbackStoryText ||
|
||||
'',
|
||||
options,
|
||||
options: params.response.projection.options.map(mapRuntimeProjectionOption),
|
||||
gameState: params.hydratedSnapshot.gameState.currentEncounter
|
||||
? params.hydratedSnapshot.gameState
|
||||
: params.fallbackGameState,
|
||||
@@ -408,14 +429,14 @@ export async function loadRuntimeInventoryView(
|
||||
}
|
||||
|
||||
export async function beginRuntimeStorySession(
|
||||
params: RuntimeStoryBootstrapRequest<
|
||||
params: BeginStoryRuntimeSessionRequest<
|
||||
GameState['customWorldProfile'],
|
||||
NonNullable<GameState['playerCharacter']>
|
||||
>,
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
const response = await requestRuntimeStoryJson<RuntimeStoryBootstrapResult>(
|
||||
'/sessions',
|
||||
const response = await requestStorySessionJson<StoryRuntimeMutationResponse>(
|
||||
'/runtime',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -425,17 +446,12 @@ export async function beginRuntimeStorySession(
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
...response,
|
||||
snapshot: rehydrateSavedSnapshot(
|
||||
response.snapshot as HydratedSavedGameSnapshot,
|
||||
),
|
||||
} satisfies RuntimeStoryBootstrapResult;
|
||||
return normalizeRuntimeMutationResponse(response);
|
||||
}
|
||||
|
||||
export async function resolveRuntimeStoryAction(
|
||||
params: {
|
||||
sessionId?: string;
|
||||
storySessionId: string;
|
||||
clientVersion?: number;
|
||||
option: Pick<StoryOption, 'functionId' | 'actionText'>;
|
||||
targetId?: string;
|
||||
@@ -443,76 +459,36 @@ export async function resolveRuntimeStoryAction(
|
||||
},
|
||||
options: RpgRuntimeStoryClientOptions = {},
|
||||
) {
|
||||
// 中文注释:story_choice 是当前前端统一提交给服务端的动作包裹格式,
|
||||
// optionText 会一起带上,方便服务端日志、提示词和调试链查看用户当轮选择。
|
||||
const response = await requestRuntimeStoryJson<RuntimeStoryResponse>(
|
||||
'/actions/resolve',
|
||||
const storySessionId = normalizeStorySessionId(
|
||||
params.storySessionId,
|
||||
'故事会话不存在,无法执行运行时动作',
|
||||
);
|
||||
// 中文注释:写入 DTO 采用 story session scoped 扁平字段;
|
||||
// optionText 仍放进 payload,方便服务端日志、提示词和调试链查看用户当轮选择。
|
||||
const response = await requestStorySessionJson<StoryRuntimeMutationResponse>(
|
||||
`/${encodeURIComponent(storySessionId)}/actions/resolve`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId: params.sessionId || DEFAULT_SESSION_ID,
|
||||
storySessionId,
|
||||
clientVersion: params.clientVersion,
|
||||
action: {
|
||||
type: 'story_choice',
|
||||
functionId: params.option.functionId,
|
||||
targetId: params.targetId,
|
||||
payload: {
|
||||
optionText: params.option.actionText,
|
||||
...(params.payload ?? {}),
|
||||
},
|
||||
functionId: params.option.functionId,
|
||||
actionText: params.option.actionText,
|
||||
targetId: params.targetId,
|
||||
payload: {
|
||||
optionText: params.option.actionText,
|
||||
...(params.payload ?? {}),
|
||||
},
|
||||
} satisfies RuntimeStoryActionRequest),
|
||||
} satisfies ResolveStoryRuntimeActionRequest),
|
||||
},
|
||||
'执行运行时动作失败',
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
...response,
|
||||
snapshot: rehydrateSavedSnapshot(
|
||||
response.snapshot as HydratedSavedGameSnapshot,
|
||||
),
|
||||
} satisfies RuntimeStoryResponse;
|
||||
return normalizeRuntimeMutationResponse(response);
|
||||
}
|
||||
|
||||
export function getRuntimeActionSnapshot(response: RuntimeStoryResponse) {
|
||||
return rehydrateSavedSnapshot(response.snapshot as HydratedSavedGameSnapshot);
|
||||
return response.snapshot;
|
||||
}
|
||||
|
||||
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