Close DDD refactor and remove generated asset proxy
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { requestJsonMock } = vi.hoisted(() => ({
|
||||
requestJsonMock: vi.fn(),
|
||||
@@ -14,25 +14,106 @@ vi.mock('../apiClient', async () => {
|
||||
});
|
||||
|
||||
import { AnimationState } from '../../types';
|
||||
import type { StoryRuntimeProjectionResponse } from '../../../packages/shared/src/contracts/story';
|
||||
import {
|
||||
beginRpgStorySession,
|
||||
beginRpgRuntimeStorySession,
|
||||
beginStorySession,
|
||||
beginRuntimeStorySession,
|
||||
buildStoryMomentFromRuntimeOptions,
|
||||
continueRpgStorySession,
|
||||
getRpgStorySessionState,
|
||||
getRpgRuntimeClientVersion,
|
||||
getRpgRuntimeSessionId,
|
||||
getRpgRuntimeStorySessionId,
|
||||
getRpgRuntimeStoryState,
|
||||
isRpgRuntimeServerFunctionId,
|
||||
isRpgRuntimeTaskFunctionId,
|
||||
loadRpgRuntimeInventoryView,
|
||||
resolveRpgRuntimeStoryProjectionMoment,
|
||||
resolveRpgRuntimeStoryAction,
|
||||
resolveRpgRuntimeStoryMoment,
|
||||
shouldUseRpgRuntimeServerOptions,
|
||||
continueStorySession,
|
||||
getStorySessionState,
|
||||
getRuntimeClientVersion,
|
||||
getRuntimeSessionId,
|
||||
getRuntimeStorySessionId,
|
||||
getRuntimeStoryState,
|
||||
isServerRuntimeFunctionId,
|
||||
isTask5RuntimeFunctionId,
|
||||
loadRuntimeInventoryView,
|
||||
buildStoryMomentFromRuntimeProjection,
|
||||
resolveRuntimeStoryAction,
|
||||
resolveRuntimeStoryMoment,
|
||||
shouldUseServerRuntimeOptions,
|
||||
} from './rpgRuntimeStoryClient';
|
||||
|
||||
type RuntimeProjectionOverrides = Omit<
|
||||
Partial<StoryRuntimeProjectionResponse>,
|
||||
'storySession'
|
||||
> & {
|
||||
storySession?: Partial<StoryRuntimeProjectionResponse['storySession']>;
|
||||
};
|
||||
|
||||
function createStorySession(
|
||||
overrides: Partial<StoryRuntimeProjectionResponse['storySession']> = {},
|
||||
): StoryRuntimeProjectionResponse['storySession'] {
|
||||
return {
|
||||
storySessionId: 'storysess-main',
|
||||
runtimeSessionId: 'runtime-main',
|
||||
actorUserId: 'user-1',
|
||||
worldProfileId: 'profile-1',
|
||||
initialPrompt: '进入营地',
|
||||
openingSummary: null,
|
||||
latestNarrativeText: '服务端故事',
|
||||
latestChoiceFunctionId: null,
|
||||
status: 'active',
|
||||
version: 1,
|
||||
createdAt: '2026-04-08T00:00:00.000Z',
|
||||
updatedAt: '2026-04-08T00:00:01.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeProjection(
|
||||
overrides: RuntimeProjectionOverrides = {},
|
||||
): StoryRuntimeProjectionResponse {
|
||||
const storySession = createStorySession(overrides.storySession);
|
||||
const serverVersion = overrides.serverVersion ?? storySession.version;
|
||||
|
||||
return {
|
||||
storySession,
|
||||
storyEvents: overrides.storyEvents ?? [],
|
||||
serverVersion,
|
||||
gameState: {
|
||||
runtimeSessionId: storySession.runtimeSessionId,
|
||||
storySessionId: storySession.storySessionId,
|
||||
runtimeActionVersion: serverVersion,
|
||||
currentScene: 'Story',
|
||||
playerEquipment: { weapon: null, armor: null, relic: null },
|
||||
...(overrides.gameState ?? {}),
|
||||
},
|
||||
actor: overrides.actor ?? {
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
mana: 20,
|
||||
maxMana: 20,
|
||||
currency: 0,
|
||||
currencyText: '0 铜钱',
|
||||
},
|
||||
inventory: overrides.inventory ?? {
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [],
|
||||
},
|
||||
options: overrides.options ?? [],
|
||||
status: overrides.status ?? {
|
||||
inBattle: false,
|
||||
npcInteractionActive: false,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
},
|
||||
currentNarrativeText:
|
||||
overrides.currentNarrativeText ?? storySession.latestNarrativeText,
|
||||
actionResultText: overrides.actionResultText ?? null,
|
||||
toast: overrides.toast ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeMutationResponse(
|
||||
overrides: RuntimeProjectionOverrides = {},
|
||||
) {
|
||||
return {
|
||||
projection: createRuntimeProjection(overrides),
|
||||
};
|
||||
}
|
||||
|
||||
describe('rpgRuntimeStoryClient', () => {
|
||||
beforeEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
@@ -64,7 +145,7 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const result = await beginRpgStorySession({
|
||||
const result = await beginStorySession({
|
||||
runtimeSessionId: 'runtime-main',
|
||||
worldProfileId: 'profile-1',
|
||||
initialPrompt: '进入营地',
|
||||
@@ -114,7 +195,7 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await continueRpgStorySession({
|
||||
await continueStorySession({
|
||||
storySessionId: ' storysess-main ',
|
||||
narrativeText: '你继续向前。',
|
||||
choiceFunctionId: 'story_continue',
|
||||
@@ -154,7 +235,7 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
storyEvents: [],
|
||||
});
|
||||
|
||||
await getRpgStorySessionState({ storySessionId: ' storysess-main ' });
|
||||
await getStorySessionState({ storySessionId: ' storysess-main ' });
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/story/sessions/storysess-main/state',
|
||||
@@ -166,25 +247,31 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('starts runtime sessions through the backend bootstrap endpoint', async () => {
|
||||
requestJsonMock.mockResolvedValue({
|
||||
sessionId: 'runtime-server-1',
|
||||
serverVersion: 1,
|
||||
snapshot: {
|
||||
version: 2,
|
||||
savedAt: '2026-04-28T00:00:00.000Z',
|
||||
bottomTab: 'adventure',
|
||||
it('starts runtime sessions through the backend runtime endpoint', async () => {
|
||||
requestJsonMock.mockResolvedValue(
|
||||
createRuntimeMutationResponse({
|
||||
storySession: {
|
||||
storySessionId: 'storysess-server-1',
|
||||
runtimeSessionId: 'runtime-server-1',
|
||||
worldProfileId: 'profile-1',
|
||||
latestNarrativeText: '营地开场',
|
||||
openingSummary: '营地开场',
|
||||
version: 2,
|
||||
updatedAt: '2026-04-28T00:00:00.000Z',
|
||||
},
|
||||
serverVersion: 2,
|
||||
gameState: {
|
||||
runtimeSessionId: 'runtime-server-1',
|
||||
storySessionId: 'storysess-server-1',
|
||||
currentScene: 'Story',
|
||||
playerCharacter: { id: 'role-1', name: '沈砺' },
|
||||
playerEquipment: { weapon: null, armor: null, relic: null },
|
||||
},
|
||||
currentStory: null,
|
||||
},
|
||||
});
|
||||
currentNarrativeText: '营地开场',
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await beginRpgRuntimeStorySession({
|
||||
const result = await beginRuntimeStorySession({
|
||||
worldType: 'CUSTOM',
|
||||
customWorldProfile: { id: 'profile-1' } as never,
|
||||
character: { id: 'role-1', name: '沈砺' } as never,
|
||||
@@ -195,8 +282,11 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
expect(result.snapshot.gameState.runtimeSessionId).toBe(
|
||||
'runtime-server-1',
|
||||
);
|
||||
expect(result.snapshot.gameState.storySessionId).toBe(
|
||||
'storysess-server-1',
|
||||
);
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/story/sessions',
|
||||
'/api/story/sessions/runtime',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -213,28 +303,35 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
it('builds runtime action requests against the dedicated story endpoint', async () => {
|
||||
requestJsonMock.mockResolvedValue({
|
||||
sessionId: 'runtime-main',
|
||||
serverVersion: 2,
|
||||
viewModel: {},
|
||||
presentation: {
|
||||
actionText: '继续交谈',
|
||||
resultText: '后端已结算',
|
||||
storyText: '后端已结算',
|
||||
options: [],
|
||||
},
|
||||
patches: [],
|
||||
snapshot: {
|
||||
version: 2,
|
||||
savedAt: '2026-04-08T00:00:00.000Z',
|
||||
bottomTab: 'adventure',
|
||||
gameState: {},
|
||||
currentStory: null,
|
||||
},
|
||||
});
|
||||
requestJsonMock.mockResolvedValue(
|
||||
createRuntimeMutationResponse({
|
||||
storySession: {
|
||||
storySessionId: 'storysess-custom',
|
||||
latestNarrativeText: '后端已结算',
|
||||
latestChoiceFunctionId: 'npc_chat',
|
||||
version: 2,
|
||||
},
|
||||
serverVersion: 2,
|
||||
gameState: {
|
||||
storySessionId: 'storysess-custom',
|
||||
runtimeSessionId: 'runtime-main',
|
||||
},
|
||||
storyEvents: [
|
||||
{
|
||||
eventId: 'storyevt-2',
|
||||
storySessionId: 'storysess-custom',
|
||||
eventKind: 'story_continued',
|
||||
narrativeText: '后端已结算',
|
||||
choiceFunctionId: 'npc_chat',
|
||||
createdAt: '2026-04-08T00:00:01.000Z',
|
||||
},
|
||||
],
|
||||
currentNarrativeText: '后端已结算',
|
||||
}),
|
||||
);
|
||||
|
||||
await resolveRpgRuntimeStoryAction({
|
||||
sessionId: 'runtime-custom',
|
||||
await resolveRuntimeStoryAction({
|
||||
storySessionId: 'storysess-custom',
|
||||
clientVersion: 9,
|
||||
option: {
|
||||
functionId: 'npc_chat',
|
||||
@@ -243,19 +340,17 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/story/actions/resolve',
|
||||
'/api/story/sessions/storysess-custom/actions/resolve',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: 'runtime-custom',
|
||||
storySessionId: 'storysess-custom',
|
||||
clientVersion: 9,
|
||||
action: {
|
||||
type: 'story_choice',
|
||||
functionId: 'npc_chat',
|
||||
targetId: undefined,
|
||||
payload: {
|
||||
optionText: '继续交谈',
|
||||
},
|
||||
functionId: 'npc_chat',
|
||||
actionText: '继续交谈',
|
||||
targetId: undefined,
|
||||
payload: {
|
||||
optionText: '继续交谈',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -265,27 +360,20 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
it('merges custom runtime payload fields into the action request body', async () => {
|
||||
requestJsonMock.mockResolvedValue({
|
||||
sessionId: 'runtime-main',
|
||||
serverVersion: 3,
|
||||
viewModel: {},
|
||||
presentation: {
|
||||
actionText: '使用凝神灵液',
|
||||
resultText: '后端已结算物品使用',
|
||||
storyText: '后端已结算物品使用',
|
||||
options: [],
|
||||
},
|
||||
patches: [],
|
||||
snapshot: {
|
||||
version: 3,
|
||||
savedAt: '2026-04-08T00:00:00.000Z',
|
||||
bottomTab: 'adventure',
|
||||
gameState: {},
|
||||
currentStory: null,
|
||||
},
|
||||
});
|
||||
requestJsonMock.mockResolvedValue(
|
||||
createRuntimeMutationResponse({
|
||||
storySession: {
|
||||
latestNarrativeText: '后端已结算物品使用',
|
||||
latestChoiceFunctionId: 'inventory_use',
|
||||
version: 3,
|
||||
},
|
||||
serverVersion: 3,
|
||||
currentNarrativeText: '后端已结算物品使用',
|
||||
}),
|
||||
);
|
||||
|
||||
await resolveRpgRuntimeStoryAction({
|
||||
await resolveRuntimeStoryAction({
|
||||
storySessionId: 'storysess-main',
|
||||
option: {
|
||||
functionId: 'inventory_use',
|
||||
actionText: '使用凝神灵液',
|
||||
@@ -296,20 +384,18 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/story/actions/resolve',
|
||||
'/api/story/sessions/storysess-main/actions/resolve',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: 'runtime-main',
|
||||
storySessionId: 'storysess-main',
|
||||
clientVersion: undefined,
|
||||
action: {
|
||||
type: 'story_choice',
|
||||
functionId: 'inventory_use',
|
||||
targetId: undefined,
|
||||
payload: {
|
||||
optionText: '使用凝神灵液',
|
||||
itemId: 'focus-tonic',
|
||||
},
|
||||
functionId: 'inventory_use',
|
||||
actionText: '使用凝神灵液',
|
||||
targetId: undefined,
|
||||
payload: {
|
||||
optionText: '使用凝神灵液',
|
||||
itemId: 'focus-tonic',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -319,49 +405,18 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
it('reads runtime story state by story session id', async () => {
|
||||
requestJsonMock.mockResolvedValue({
|
||||
storySession: {
|
||||
storySessionId: 'storysess-main',
|
||||
runtimeSessionId: 'runtime-main',
|
||||
actorUserId: 'user-1',
|
||||
worldProfileId: 'profile-1',
|
||||
initialPrompt: '进入营地',
|
||||
openingSummary: null,
|
||||
latestNarrativeText: '服务端故事',
|
||||
latestChoiceFunctionId: null,
|
||||
status: 'active',
|
||||
version: 4,
|
||||
createdAt: '2026-04-08T00:00:00.000Z',
|
||||
updatedAt: '2026-04-08T00:00:00.000Z',
|
||||
},
|
||||
storyEvents: [],
|
||||
serverVersion: 4,
|
||||
actor: {
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
mana: 20,
|
||||
maxMana: 20,
|
||||
currency: 0,
|
||||
currencyText: '0 铜钱',
|
||||
},
|
||||
inventory: {
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [],
|
||||
},
|
||||
options: [],
|
||||
status: {
|
||||
inBattle: false,
|
||||
npcInteractionActive: false,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
},
|
||||
currentNarrativeText: '服务端故事',
|
||||
actionResultText: null,
|
||||
toast: null,
|
||||
});
|
||||
requestJsonMock.mockResolvedValue(
|
||||
createRuntimeProjection({
|
||||
storySession: {
|
||||
latestNarrativeText: '服务端故事',
|
||||
version: 4,
|
||||
},
|
||||
serverVersion: 4,
|
||||
currentNarrativeText: '服务端故事',
|
||||
}),
|
||||
);
|
||||
|
||||
await getRpgRuntimeStoryState({
|
||||
await getRuntimeStoryState({
|
||||
storySessionId: 'storysess-main',
|
||||
clientVersion: 7,
|
||||
});
|
||||
@@ -378,13 +433,13 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
|
||||
it('rejects missing story session id instead of falling back to runtime id', async () => {
|
||||
expect(() =>
|
||||
getRpgRuntimeStorySessionId({
|
||||
getRuntimeStorySessionId({
|
||||
storySessionId: '',
|
||||
}),
|
||||
).toThrow('运行时故事会话不存在,无法读取服务端投影');
|
||||
|
||||
await expect(
|
||||
loadRpgRuntimeInventoryView({
|
||||
loadRuntimeInventoryView({
|
||||
gameState: {
|
||||
runtimeSessionId: 'runtime-inventory',
|
||||
storySessionId: null,
|
||||
@@ -394,14 +449,14 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
).rejects.toThrow('运行时故事会话不存在,无法读取服务端投影');
|
||||
|
||||
await expect(
|
||||
continueRpgStorySession({
|
||||
continueStorySession({
|
||||
storySessionId: '',
|
||||
narrativeText: '继续',
|
||||
}),
|
||||
).rejects.toThrow('故事会话不存在,无法继续故事');
|
||||
|
||||
await expect(
|
||||
getRpgStorySessionState({
|
||||
getStorySessionState({
|
||||
storySessionId: '',
|
||||
}),
|
||||
).rejects.toThrow('故事会话不存在,无法读取故事会话状态');
|
||||
@@ -410,74 +465,62 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
it('loads backend inventory view from story runtime projection', async () => {
|
||||
requestJsonMock.mockResolvedValue({
|
||||
storySession: {
|
||||
storySessionId: 'storysess-inventory',
|
||||
runtimeSessionId: 'runtime-inventory',
|
||||
actorUserId: 'user-1',
|
||||
worldProfileId: 'profile-1',
|
||||
initialPrompt: '进入营地',
|
||||
openingSummary: null,
|
||||
latestNarrativeText: '背包状态',
|
||||
latestChoiceFunctionId: null,
|
||||
status: 'active',
|
||||
version: 5,
|
||||
createdAt: '2026-04-08T00:00:00.000Z',
|
||||
updatedAt: '2026-04-08T00:00:00.000Z',
|
||||
},
|
||||
storyEvents: [],
|
||||
serverVersion: 5,
|
||||
actor: {
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
mana: 20,
|
||||
maxMana: 20,
|
||||
currency: 90,
|
||||
currencyText: '90 铜钱',
|
||||
},
|
||||
inventory: {
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [
|
||||
{
|
||||
id: 'synthesis-refined-ingot',
|
||||
name: '压炼锭材',
|
||||
kind: 'synthesis',
|
||||
description: '把零散残片和基础材料压成稳定可用的金属锭材。',
|
||||
resultLabel: '精炼锭材',
|
||||
currencyCost: 18,
|
||||
currencyText: '18 铜钱',
|
||||
requirements: [
|
||||
{
|
||||
id: 'material:any',
|
||||
label: '任意材料',
|
||||
quantity: 3,
|
||||
owned: 3,
|
||||
requestJsonMock.mockResolvedValue(
|
||||
createRuntimeProjection({
|
||||
storySession: {
|
||||
storySessionId: 'storysess-inventory',
|
||||
runtimeSessionId: 'runtime-inventory',
|
||||
latestNarrativeText: '背包状态',
|
||||
version: 5,
|
||||
},
|
||||
serverVersion: 5,
|
||||
gameState: {
|
||||
storySessionId: 'storysess-inventory',
|
||||
runtimeSessionId: 'runtime-inventory',
|
||||
},
|
||||
actor: {
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
mana: 20,
|
||||
maxMana: 20,
|
||||
currency: 90,
|
||||
currencyText: '90 铜钱',
|
||||
},
|
||||
inventory: {
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [
|
||||
{
|
||||
id: 'synthesis-refined-ingot',
|
||||
name: '压炼锭材',
|
||||
kind: 'synthesis',
|
||||
description: '把零散残片和基础材料压成稳定可用的金属锭材。',
|
||||
resultLabel: '精炼锭材',
|
||||
currencyCost: 18,
|
||||
currencyText: '18 铜钱',
|
||||
requirements: [
|
||||
{
|
||||
id: 'material:any',
|
||||
label: '任意材料',
|
||||
quantity: 3,
|
||||
owned: 3,
|
||||
},
|
||||
],
|
||||
canCraft: true,
|
||||
action: {
|
||||
functionId: 'forge_craft',
|
||||
actionText: '制作精炼锭材',
|
||||
payload: { recipeId: 'synthesis-refined-ingot' },
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
canCraft: true,
|
||||
action: {
|
||||
functionId: 'forge_craft',
|
||||
actionText: '制作精炼锭材',
|
||||
payload: { recipeId: 'synthesis-refined-ingot' },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: [],
|
||||
status: {
|
||||
inBattle: false,
|
||||
npcInteractionActive: false,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
},
|
||||
currentNarrativeText: '',
|
||||
actionResultText: null,
|
||||
toast: null,
|
||||
});
|
||||
],
|
||||
},
|
||||
currentNarrativeText: '',
|
||||
}),
|
||||
);
|
||||
|
||||
const view = await loadRpgRuntimeInventoryView({
|
||||
const view = await loadRuntimeInventoryView({
|
||||
gameState: {
|
||||
storySessionId: 'storysess-inventory',
|
||||
runtimeActionVersion: 5,
|
||||
@@ -524,13 +567,13 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
it('recognizes server-runtime option pools for server-side legality checks', () => {
|
||||
expect(isRpgRuntimeTaskFunctionId('npc_chat')).toBe(true);
|
||||
expect(isRpgRuntimeTaskFunctionId('battle_attack_basic')).toBe(true);
|
||||
expect(isRpgRuntimeTaskFunctionId('npc_trade')).toBe(false);
|
||||
expect(isRpgRuntimeServerFunctionId('npc_trade')).toBe(true);
|
||||
expect(isRpgRuntimeServerFunctionId('unknown_action')).toBe(false);
|
||||
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);
|
||||
expect(
|
||||
shouldUseRpgRuntimeServerOptions([
|
||||
shouldUseServerRuntimeOptions([
|
||||
{
|
||||
functionId: 'npc_chat',
|
||||
actionText: '继续交谈',
|
||||
@@ -547,7 +590,7 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
]),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldUseRpgRuntimeServerOptions([
|
||||
shouldUseServerRuntimeOptions([
|
||||
{
|
||||
functionId: 'npc_trade',
|
||||
actionText: '交易',
|
||||
@@ -564,7 +607,7 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
]),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldUseRpgRuntimeServerOptions([
|
||||
shouldUseServerRuntimeOptions([
|
||||
{
|
||||
functionId: 'unknown_action',
|
||||
actionText: '未知动作',
|
||||
@@ -580,47 +623,25 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
},
|
||||
]),
|
||||
).toBe(false);
|
||||
expect(getRpgRuntimeSessionId({ runtimeSessionId: '' })).toBe(
|
||||
expect(getRuntimeSessionId({ runtimeSessionId: '' })).toBe(
|
||||
'runtime-main',
|
||||
);
|
||||
expect(getRpgRuntimeStorySessionId({ storySessionId: ' storysess-1 ' })).toBe(
|
||||
expect(getRuntimeStorySessionId({ storySessionId: ' storysess-1 ' })).toBe(
|
||||
'storysess-1',
|
||||
);
|
||||
expect(getRpgRuntimeClientVersion({ runtimeActionVersion: 3 })).toBe(3);
|
||||
expect(getRuntimeClientVersion({ runtimeActionVersion: 3 })).toBe(3);
|
||||
});
|
||||
|
||||
it('builds story moments from story runtime projection options', () => {
|
||||
const story = resolveRpgRuntimeStoryProjectionMoment({
|
||||
projection: {
|
||||
const story = buildStoryMomentFromRuntimeProjection({
|
||||
projection: createRuntimeProjection({
|
||||
storySession: {
|
||||
storySessionId: 'storysess-main',
|
||||
runtimeSessionId: 'runtime-main',
|
||||
actorUserId: 'user-1',
|
||||
worldProfileId: 'profile-1',
|
||||
initialPrompt: '进入营地',
|
||||
openingSummary: null,
|
||||
latestNarrativeText: '兜底故事',
|
||||
latestChoiceFunctionId: null,
|
||||
status: 'active',
|
||||
version: 5,
|
||||
createdAt: '2026-04-08T00:00:00.000Z',
|
||||
updatedAt: '2026-04-08T00:00:00.000Z',
|
||||
},
|
||||
storyEvents: [],
|
||||
serverVersion: 5,
|
||||
actor: {
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
mana: 20,
|
||||
maxMana: 20,
|
||||
currency: 0,
|
||||
currencyText: '0 铜钱',
|
||||
},
|
||||
inventory: {
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
functionId: 'npc_chat',
|
||||
@@ -639,9 +660,7 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
currentNpcBattleOutcome: null,
|
||||
},
|
||||
currentNarrativeText: '服务端投影故事',
|
||||
actionResultText: null,
|
||||
toast: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(story.text).toBe('服务端投影故事');
|
||||
@@ -680,100 +699,65 @@ describe('rpgRuntimeStoryClient', () => {
|
||||
});
|
||||
|
||||
it('prefers the richer snapshot story when the server persisted dialogue mode', () => {
|
||||
const story = resolveRpgRuntimeStoryMoment({
|
||||
const hydratedSnapshot = {
|
||||
version: 2,
|
||||
savedAt: '2026-04-08T00:00:00.000Z',
|
||||
bottomTab: 'adventure',
|
||||
gameState: {} as never,
|
||||
currentStory: {
|
||||
text: '你:先把话说开。\n梁伯:那我就直说了。',
|
||||
options: [],
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{ speaker: 'player', text: '先把话说开。' },
|
||||
{ speaker: 'npc', speakerName: '梁伯', text: '那我就直说了。' },
|
||||
],
|
||||
deferredOptions: [
|
||||
{
|
||||
functionId: 'npc_chat',
|
||||
actionText: '继续交谈',
|
||||
text: '继续交谈',
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
playerMoveMeters: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
scrollWorld: false,
|
||||
monsterChanges: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as never;
|
||||
const story = resolveRuntimeStoryMoment({
|
||||
response: {
|
||||
sessionId: 'runtime-main',
|
||||
serverVersion: 4,
|
||||
viewModel: {
|
||||
player: { hp: 10, maxHp: 10, mana: 5, maxMana: 5 },
|
||||
encounter: null,
|
||||
companions: [],
|
||||
inventory: {
|
||||
playerCurrency: 0,
|
||||
currencyText: '0 铜钱',
|
||||
inBattle: false,
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [],
|
||||
},
|
||||
availableOptions: [],
|
||||
status: {
|
||||
inBattle: false,
|
||||
npcInteractionActive: true,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
projection: createRuntimeProjection({
|
||||
storySession: {
|
||||
latestNarrativeText: '普通文本',
|
||||
latestChoiceFunctionId: 'npc_chat',
|
||||
version: 4,
|
||||
},
|
||||
serverVersion: 4,
|
||||
currentNarrativeText: '普通文本',
|
||||
}),
|
||||
snapshot: hydratedSnapshot,
|
||||
inventoryView: {
|
||||
playerCurrency: 0,
|
||||
currencyText: '0 铜钱',
|
||||
inBattle: false,
|
||||
backpackItems: [],
|
||||
equipmentSlots: [],
|
||||
forgeRecipes: [],
|
||||
},
|
||||
presentation: {
|
||||
actionText: '继续交谈',
|
||||
resultText: '后端已结算',
|
||||
storyText: '普通文本',
|
||||
options: [],
|
||||
battle: null,
|
||||
toast: null,
|
||||
},
|
||||
patches: [],
|
||||
snapshot: {
|
||||
version: 2,
|
||||
savedAt: '2026-04-08T00:00:00.000Z',
|
||||
bottomTab: 'adventure',
|
||||
gameState: {} as never,
|
||||
currentStory: {
|
||||
text: '你:先把话说开。\n梁伯:那我就直说了。',
|
||||
options: [],
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{ speaker: 'player', text: '先把话说开。' },
|
||||
{ speaker: 'npc', speakerName: '梁伯', text: '那我就直说了。' },
|
||||
],
|
||||
deferredOptions: [
|
||||
{
|
||||
functionId: 'npc_chat',
|
||||
actionText: '继续交谈',
|
||||
text: '继续交谈',
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
playerMoveMeters: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
scrollWorld: false,
|
||||
monsterChanges: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
hydratedSnapshot: {
|
||||
version: 2,
|
||||
savedAt: '2026-04-08T00:00:00.000Z',
|
||||
bottomTab: 'adventure',
|
||||
gameState: {} as never,
|
||||
currentStory: {
|
||||
text: '你:先把话说开。\n梁伯:那我就直说了。',
|
||||
options: [],
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{ speaker: 'player', text: '先把话说开。' },
|
||||
{ speaker: 'npc', speakerName: '梁伯', text: '那我就直说了。' },
|
||||
],
|
||||
deferredOptions: [
|
||||
{
|
||||
functionId: 'npc_chat',
|
||||
actionText: '继续交谈',
|
||||
text: '继续交谈',
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
playerMoveMeters: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
scrollWorld: false,
|
||||
monsterChanges: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as never,
|
||||
hydratedSnapshot,
|
||||
fallbackStoryText: '普通文本',
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user