This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -135,7 +135,7 @@ describe('runtimeStoryService', () => {
);
});
it('filters disabled runtime options when rebuilding a story moment', () => {
it('keeps disabled runtime options when rebuilding a story moment', () => {
const story = buildStoryMomentFromRuntimeOptions({
storyText: '服务端返回的新故事',
options: [
@@ -155,12 +155,16 @@ describe('runtimeStoryService', () => {
});
expect(story.text).toBe('服务端返回的新故事');
expect(story.options).toHaveLength(1);
expect(story.options).toHaveLength(2);
expect(story.options[0]?.functionId).toBe('npc_chat');
expect(story.options[1]?.functionId).toBe('npc_recruit');
expect(story.options[1]?.disabled).toBe(true);
expect(story.options[1]?.disabledReason).toBe('队伍已满');
});
it('recognizes server-runtime option pools for server-side legality checks', () => {
expect(isTask5RuntimeFunctionId('npc_chat')).toBe(true);
expect(isTask5RuntimeFunctionId('battle_attack_basic')).toBe(true);
expect(isTask5RuntimeFunctionId('npc_trade')).toBe(false);
expect(isServerRuntimeFunctionId('npc_trade')).toBe(true);
expect(isServerRuntimeFunctionId('unknown_action')).toBe(false);

View File

@@ -103,15 +103,11 @@ function createRuntimeStoryOption(
option: RuntimeStoryOptionView,
gameState?: Pick<GameState, 'currentEncounter'>,
): StoryOption {
const detailParts = [option.detailText, option.disabled ? option.reason : null]
.filter(Boolean)
.join(' ');
return {
functionId: option.functionId,
actionText: option.actionText,
text: option.actionText,
detailText: detailParts || undefined,
detailText: option.detailText,
visuals: {
playerAnimation: AnimationState.IDLE,
playerMoveMeters: 0,
@@ -121,6 +117,9 @@ function createRuntimeStoryOption(
monsterChanges: [],
},
interaction: buildRuntimeOptionInteraction(option, gameState),
runtimePayload: option.payload,
disabled: option.disabled,
disabledReason: option.reason,
};
}
@@ -162,9 +161,9 @@ export function buildStoryMomentFromRuntimeOptions(params: {
}) {
return {
text: params.storyText,
options: params.options
.filter((option) => !option.disabled)
.map((option) => createRuntimeStoryOption(option, params.gameState)),
options: params.options.map((option) =>
createRuntimeStoryOption(option, params.gameState),
),
} satisfies StoryMoment;
}

View File

@@ -7,6 +7,8 @@ const { requestJsonMock } = vi.hoisted(() => ({
import {
clearProfileBrowseHistory,
listProfileBrowseHistory,
listProfileSaveArchives,
resumeProfileSaveArchive,
syncProfileBrowseHistory,
upsertProfileBrowseHistory,
} from './storageService';
@@ -103,3 +105,54 @@ describe('storageService browse history routes', () => {
);
});
});
describe('storageService save archive routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads save archives from the runtime profile route', async () => {
await listProfileSaveArchives();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/save-archives',
expect.objectContaining({ method: 'GET' }),
'读取存档列表失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('resumes a save archive through the runtime profile route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
worldKey: 'custom:world-1',
},
snapshot: {
version: 2,
savedAt: '2026-04-19T10:15:00.000Z',
bottomTab: 'adventure',
currentStory: null,
gameState: {
worldType: 'CUSTOM',
},
},
});
await resumeProfileSaveArchive('custom:world-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/profile/save-archives/custom%3Aworld-1',
expect.objectContaining({ method: 'POST' }),
'恢复存档失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});

View File

@@ -11,6 +11,9 @@ import type {
PlatformBrowseHistoryResponse,
PlatformBrowseHistoryWriteEntry,
ProfileDashboardSummary,
ProfileSaveArchiveListResponse,
ProfileSaveArchiveResumeResponse,
ProfileSaveArchiveSummary,
ProfilePlayStatsResponse,
ProfileWalletLedgerResponse,
RuntimeSettings,
@@ -137,6 +140,40 @@ export async function getProfilePlayStats(options: RuntimeRequestOptions = {}) {
);
}
export async function listProfileSaveArchives(
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<ProfileSaveArchiveListResponse>(
'/profile/save-archives',
{ method: 'GET' },
'读取存档列表失败',
options,
);
return Array.isArray(response?.entries) ? response.entries : [];
}
export async function resumeProfileSaveArchive(
worldKey: string,
options: RuntimeRequestOptions = {},
) {
const response = await requestRuntimeJson<
ProfileSaveArchiveResumeResponse
>(
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复存档失败',
options,
);
return {
entry: response.entry,
snapshot: rehydrateSavedSnapshot(
response.snapshot as HydratedSavedGameSnapshot,
),
};
}
export async function putSettings(
settings: RuntimeSettings,
options: RuntimeRequestOptions = {},
@@ -363,6 +400,8 @@ export const runtimeStorageClient = {
getProfileDashboard,
getProfileWalletLedger,
getProfilePlayStats,
listProfileSaveArchives,
resumeProfileSaveArchive,
listCustomWorldLibrary,
listCustomWorldWorks,
upsertCustomWorldProfile,
@@ -379,3 +418,4 @@ export const runtimeStorageClient = {
export type { CustomWorldLibraryEntry };
export type { PlatformBrowseHistoryEntry };
export type { ProfileSaveArchiveSummary };