1
This commit is contained in:
@@ -895,7 +895,6 @@ function normalizePlayableNpcList(value: unknown) {
|
||||
titleFallback: '未定称号',
|
||||
defaultAffinity: DEFAULT_PLAYABLE_INITIAL_AFFINITY,
|
||||
}),
|
||||
templateCharacterId: toText(item.templateCharacterId) || undefined,
|
||||
}))
|
||||
.filter((entry) => entry.name)
|
||||
.slice(0, MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT);
|
||||
|
||||
@@ -118,12 +118,10 @@ export function buildExpandedCustomWorldProfile(
|
||||
const playableNpcs = dedupeByName(profile.playableNpcs)
|
||||
.slice(0, PLAYABLE_TEMPLATE_CHARACTER_IDS.length)
|
||||
.map((npc, index) => {
|
||||
const templateCharacterId =
|
||||
npc.templateCharacterId ?? getPlayableTemplateCharacterId(index);
|
||||
const templateCharacterId = getPlayableTemplateCharacterId(index);
|
||||
return {
|
||||
...npc,
|
||||
id: npc.id || createEntryId('playable-npc', npc.name, index),
|
||||
templateCharacterId,
|
||||
tags: mergeCustomWorldPlayableNpcTags(profile, npc, {
|
||||
templateCharacterId,
|
||||
maxCount: 5,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ROLE_TEMPLATE_CHARACTERS } from '../data/characterPresets';
|
||||
import type {
|
||||
CustomWorldCoverProfile,
|
||||
CustomWorldPlayableNpc,
|
||||
@@ -43,15 +42,7 @@ function resolvePlayableCoverImageSrc(role: CustomWorldPlayableNpc) {
|
||||
return explicitImageSrc;
|
||||
}
|
||||
|
||||
if (!role.templateCharacterId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
ROLE_TEMPLATE_CHARACTERS.find(
|
||||
(character) => character.id === role.templateCharacterId,
|
||||
)?.portrait ?? null
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeCoverCharacterRoleIds(
|
||||
|
||||
@@ -378,9 +378,7 @@ function buildRoleArchetypes(profile: CustomWorldProfile) {
|
||||
narrativeFunction:
|
||||
role.role.trim() || role.description.trim() || '在主线推进中提供关键响应。',
|
||||
sourceRoleIds: [role.id],
|
||||
sourceTemplateCharacterIds: role.templateCharacterId
|
||||
? [role.templateCharacterId]
|
||||
: [],
|
||||
sourceTemplateCharacterIds: [],
|
||||
tags: dedupeStrings(role.tags, 5),
|
||||
})) satisfies RoleArchetypeProfile[];
|
||||
}
|
||||
|
||||
@@ -32,13 +32,37 @@ const sessionWithPreview: CustomWorldAgentSessionSnapshot = {
|
||||
anchorPack: null,
|
||||
lockState: null,
|
||||
draftProfile: {
|
||||
id: 'draft-profile-1',
|
||||
settingText: '草稿 profile 直接进入游戏。',
|
||||
name: '只作为 fallback 的本地草稿名',
|
||||
subtitle: 'fallback',
|
||||
summary: 'fallback',
|
||||
tone: 'fallback',
|
||||
playerGoal: 'fallback',
|
||||
playableNpcs: [],
|
||||
templateWorldType: 'WUXIA',
|
||||
majorFactions: [],
|
||||
coreConflicts: [],
|
||||
playableNpcs: [
|
||||
{
|
||||
id: 'draft-playable-1',
|
||||
name: '草稿角色',
|
||||
title: '直读测试',
|
||||
role: '可扮演角色',
|
||||
description: '从 draftProfile 直接进入角色选择页。',
|
||||
backstory: '草稿角色的背景不经过 resultPreview 转换。',
|
||||
personality: '直接、清醒',
|
||||
motivation: '验证草稿直读链路',
|
||||
combatStyle: '以直读链路破局',
|
||||
initialAffinity: 18,
|
||||
relationshipHooks: ['来自草稿'],
|
||||
tags: ['draft-profile'],
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
imageSrc: '/generated-characters/draft-playable-1/portrait.png',
|
||||
},
|
||||
],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [],
|
||||
},
|
||||
messages: [],
|
||||
@@ -103,19 +127,21 @@ test('buildRpgCreationPreviewFromResultPreview normalizes server preview envelop
|
||||
expect(profile?.settingText).toBe('被海雾吞没的旧航路群岛');
|
||||
});
|
||||
|
||||
test('buildRpgCreationPreviewFromSession prefers server resultPreview over draft fallback', () => {
|
||||
test('buildRpgCreationPreviewFromSession reads draftProfile directly', () => {
|
||||
const profile = buildRpgCreationPreviewFromSession(sessionWithPreview);
|
||||
|
||||
expect(profile?.name).toBe('服务端结果预览');
|
||||
expect(profile?.name).not.toBe('只作为 fallback 的本地草稿名');
|
||||
expect(profile?.summary).toBe('结果页应该优先消费 session.resultPreview。');
|
||||
expect(profile?.name).toBe('只作为 fallback 的本地草稿名');
|
||||
expect(profile?.name).not.toBe('服务端结果预览');
|
||||
expect(profile?.playableNpcs[0]?.imageSrc).toBe(
|
||||
'/generated-characters/draft-playable-1/portrait.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildRpgCreationPreviewFromSession returns null when server resultPreview is missing', () => {
|
||||
test('buildRpgCreationPreviewFromSession does not require resultPreview', () => {
|
||||
const profile = buildRpgCreationPreviewFromSession({
|
||||
...sessionWithPreview,
|
||||
resultPreview: null,
|
||||
});
|
||||
|
||||
expect(profile).toBeNull();
|
||||
expect(profile?.name).toBe('只作为 fallback 的本地草稿名');
|
||||
});
|
||||
|
||||
@@ -2,10 +2,6 @@ import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/s
|
||||
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
/**
|
||||
* Phase 5 起结果页只消费服务端回传的 result preview。
|
||||
* 前端不再承担 session draft -> runtime profile 的本地兼容编译职责。
|
||||
*/
|
||||
export function buildCustomWorldProfileFromResultPreview(
|
||||
resultPreview: CustomWorldAgentSessionSnapshot['resultPreview'] | null | undefined,
|
||||
): CustomWorldProfile | null {
|
||||
@@ -13,20 +9,18 @@ export function buildCustomWorldProfileFromResultPreview(
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一“从 session 取结果页 profile”的主入口。
|
||||
* Phase 5 后主链没有 preview 就视为服务端未准备完成,而不是继续做前端本地编译。
|
||||
* RPG 运行时直接读取 Agent session 的 draftProfile。
|
||||
* resultPreview 只作为质量/发布信息外壳,不再参与进入游戏 profile 的数据转换。
|
||||
*/
|
||||
export function buildCustomWorldProfileFromAgentSession(
|
||||
session: CustomWorldAgentSessionSnapshot | null | undefined,
|
||||
): CustomWorldProfile | null {
|
||||
return buildCustomWorldProfileFromResultPreview(session?.resultPreview);
|
||||
return normalizeCustomWorldProfileRecord(session?.draftProfile ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 这是工作包 A 提供的新命名兼容层。
|
||||
* Phase 3 后该适配层只负责:
|
||||
* 1. 把服务端 resultPreview 转成前端 view model
|
||||
* 2. 保持前端 session 读模型入口稳定
|
||||
* 主入口保持命名稳定,但数据来源已经收敛为 draftProfile 单一真相源。
|
||||
*/
|
||||
export const rpgCreationPreviewAdapter = {
|
||||
buildPreviewFromSession: buildCustomWorldProfileFromAgentSession,
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('campaignPackCompiler', () => {
|
||||
storyGraph: {
|
||||
visibleThreads: [{ id: 'thread-1', title: '封桥旧案' }],
|
||||
},
|
||||
playableNpcs: [{ id: 'npc-1', templateCharacterId: 'archer-hero' }],
|
||||
playableNpcs: [{ id: 'npc-1' }],
|
||||
} as unknown as CustomWorldProfile;
|
||||
|
||||
const compiled = compileCampaignFromWorldProfile({ profile });
|
||||
|
||||
@@ -48,7 +48,7 @@ export function buildCampaignPack(params: {
|
||||
authoringStyle,
|
||||
campaignStateSeed,
|
||||
actTemplates,
|
||||
requiredCompanionIds: profile.playableNpcs.slice(0, 2).map((npc) => npc.templateCharacterId ?? npc.id),
|
||||
requiredCompanionIds: profile.playableNpcs.slice(0, 2).map((npc) => npc.id),
|
||||
} satisfies CampaignPack;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user