import { describe, expect, it } from 'vitest'; import { normalizeCustomWorldProfileRecord } from './customWorldLibrary'; describe('normalizeCustomWorldProfileRecord role asset descriptions', () => { it('保留草稿生成阶段产出的角色形象描述字段', () => { const profile = normalizeCustomWorldProfileRecord({ name: '雾港归航', settingText: '海雾旧案', playableNpcs: [ { name: '岑灯', title: '返乡守灯人', role: '主角代理', description: '追查旧案的人', visualDescription: '瘦高守灯人披深蓝旧雨衣,腰挂铜灯与卷边海图,眼下有长期失眠的青影。', actionDescription: '抬灯照出雾中航线,侧身抽出卷边海图迅速标记。', sceneVisualDescription: '旧灯塔石阶被潮水打湿,青白灯火照着雾中海图。', }, ], storyNpcs: [ { name: '议长甲', title: '群岛议长', role: '遮掩者', description: '压住旧档的人', visualDescription: '银发议长穿硬挺黑色长礼服,胸前别着海鸟徽章,手套边缘沾着档案灰。', actionDescription: '用印信压住卷宗,抬手示意巡海队封锁出口。', sceneVisualDescription: '议会厅高窗外翻涌海雾,长桌尽头堆着封存卷宗。', }, ], }); expect(profile?.playableNpcs[0]?.visualDescription).toBe( '瘦高守灯人披深蓝旧雨衣,腰挂铜灯与卷边海图,眼下有长期失眠的青影。', ); expect(profile?.playableNpcs[0]?.actionDescription).toContain('抬灯'); expect(profile?.playableNpcs[0]?.sceneVisualDescription).toContain('旧灯塔'); expect(profile?.storyNpcs[0]?.visualDescription).toBe( '银发议长穿硬挺黑色长礼服,胸前别着海鸟徽章,手套边缘沾着档案灰。', ); expect(profile?.storyNpcs[0]?.actionDescription).toContain('印信'); expect(profile?.storyNpcs[0]?.sceneVisualDescription).toContain('议会厅'); }); it('保留 Agent 发布门槛需要的顶层 worldHook 和 playerPremise', () => { const profile = normalizeCustomWorldProfileRecord({ name: '雾港归航', settingText: '海雾旧案', summary: '海雾会吞掉记错航线的人。', worldHook: '在失真的海图上追查一场被篡改的沉船事故。', playerPremise: '玩家是返乡调查旧案的守灯人。', sceneChapterBlueprints: [ { id: 'scene-chapter-1', sceneId: 'landmark-1', title: '失灯港', acts: [ { id: 'act-1', title: '第一幕', summary: '玩家在雾港发现灯册被改写。', }, ], }, ], }); expect(profile?.worldHook).toBe( '在失真的海图上追查一场被篡改的沉船事故。', ); expect(profile?.playerPremise).toBe('玩家是返乡调查旧案的守灯人。'); expect(profile?.sceneChapterBlueprints?.[0]?.acts).toHaveLength(1); }); it('把幕配置里的角色名归一到真实角色 id', () => { const profile = normalizeCustomWorldProfileRecord({ name: '雾港归航', settingText: '海雾旧案', playableNpcs: [ { id: 'playable-cendeng', name: '岑灯', title: '返乡守灯人', role: '主角代理', }, ], storyNpcs: [ { id: 'story-luheng', name: '陆衡', title: '航运公会审计员', role: '第一幕主NPC', }, ], sceneChapterBlueprints: [ { id: 'scene-chapter-1', sceneId: 'custom-scene-camp', title: '开局章节', acts: [ { id: 'act-1', title: '第一幕', summary: '陆衡先拦住玩家。', encounterNpcIds: ['陆衡'], primaryNpcId: '航运公会审计员', oppositeNpcId: '陆衡', }, ], }, ], }); const act = profile?.sceneChapterBlueprints?.[0]?.acts[0]; expect(act?.encounterNpcIds).toEqual(['story-luheng']); expect(act?.primaryNpcId).toBe('story-luheng'); expect(act?.oppositeNpcId).toBe('story-luheng'); }); it('直接读取 Rust 草稿角色字段和形象资源', () => { const profile = normalizeCustomWorldProfileRecord({ name: '雾港归航', settingText: '海雾旧案', playableNpcs: [ { id: 'playable-cendeng', name: '岑灯', title: '返乡守灯人', role: '主角代理', publicMask: '深蓝旧雨衣、铜灯和卷边海图。', currentPressure: '灯塔记录被人改写,旧案正在逼近。', relationToPlayer: '这是玩家进入世界的第一视角。', imageSrc: '/generated-characters/playable-cendeng/portrait.png', generatedVisualAssetId: 'visual-playable-cendeng', }, ], storyNpcs: [ { id: 'story-yizhang', name: '议长甲', title: '群岛议长', role: '遮掩者', publicIdentity: '压住旧档的人。', hiddenHook: '长期维持群岛议会体面并遮掩沉船旧案。', relationToPlayer: '会阻止玩家继续追查。', imageSrc: '/generated-characters/story-yizhang/portrait.png', }, ], }); expect(profile?.playableNpcs[0]?.description).toBe( '深蓝旧雨衣、铜灯和卷边海图。', ); expect(profile?.playableNpcs[0]?.backstory).toContain('灯塔记录'); expect(profile?.playableNpcs[0]?.relationshipHooks[0]).toBe( '这是玩家进入世界的第一视角。', ); expect(profile?.playableNpcs[0]?.imageSrc).toBe( '/generated-characters/playable-cendeng/portrait.png', ); expect(profile?.storyNpcs[0]?.description).toBe('压住旧档的人。'); expect(profile?.storyNpcs[0]?.backstory).toContain('沉船旧案'); expect(profile?.storyNpcs[0]?.imageSrc).toBe( '/generated-characters/story-yizhang/portrait.png', ); }); it('保留结果页生成的开局 CG 槽位', () => { const profile = normalizeCustomWorldProfileRecord({ name: '雾港归航', settingText: '海雾旧案', openingCg: { id: 'opening-cg-1', status: 'ready', storyboardImageSrc: '/generated-custom-world-scenes/opening/storyboard.png', storyboardAssetId: 'storyboard-1', videoSrc: '/generated-custom-world-scenes/opening/opening.mp4', videoAssetId: 'video-1', imageModel: 'gpt-image-2', videoModel: 'doubao-seedance-2-0-fast-260128', aspectRatio: '16:9', imageSize: '2k', videoResolution: '480p', durationSeconds: 15, pointCost: 80, estimatedWaitMinutes: 10, updatedAt: '2026-05-21T00:00:00.000Z', }, }); expect(profile?.openingCg?.videoSrc).toBe( '/generated-custom-world-scenes/opening/opening.mp4', ); expect(profile?.openingCg?.storyboardImageSrc).toBe( '/generated-custom-world-scenes/opening/storyboard.png', ); }); it('保留结果页封面和关键图片资产槽位', () => { const profile = normalizeCustomWorldProfileRecord({ name: '星砂废都', settingText: '坠星沙海与废都钟楼', cover: { sourceType: 'generated', imageSrc: '/generated-custom-world-covers/star-waste/cover.webp', characterRoleIds: ['playable-shamian'], }, camp: { id: 'camp-star-waste', name: '废都营地', description: '钟楼阴影下的临时营地。', imageSrc: '/assets/custom-world/camp-star-waste.png', sceneNpcIds: ['playable-shamian'], connections: [], }, playableNpcs: [ { id: 'playable-shamian', name: '砂眠', title: '废都引路人', role: '主角代理', imageSrc: '/assets/custom-world/playable-shamian.png', }, ], storyNpcs: [ { id: 'story-clock-keeper', name: '钟守', title: '钟楼守夜者', role: '第一幕主NPC', imageSrc: '/assets/custom-world/story-clock-keeper.png', }, ], landmarks: [ { id: 'landmark-clocktower', name: '坠星钟楼', description: '半截钟楼被星砂埋住。', imageSrc: '/assets/custom-world/landmark-clocktower.png', sceneNpcIds: ['story-clock-keeper'], connections: [], }, ], sceneChapterBlueprints: [ { id: 'scene-chapter-clocktower', sceneId: 'landmark-clocktower', title: '钟楼第一夜', acts: [ { id: 'act-clocktower-opening', sceneId: 'landmark-clocktower', title: '第一幕', summary: '砂眠带玩家进入坠星钟楼。', backgroundImageSrc: '/assets/custom-world/act-clocktower-opening.png', encounterNpcIds: ['砂眠', '钟守'], primaryNpcId: '砂眠', oppositeNpcId: '钟守', }, ], }, ], }); expect(profile?.cover?.sourceType).toBe('generated'); expect(profile?.cover?.imageSrc).toBe( '/generated-custom-world-covers/star-waste/cover.webp', ); expect(profile?.camp?.imageSrc).toBe( '/assets/custom-world/camp-star-waste.png', ); expect(profile?.playableNpcs[0]?.imageSrc).toBe( '/assets/custom-world/playable-shamian.png', ); expect(profile?.storyNpcs[0]?.imageSrc).toBe( '/assets/custom-world/story-clock-keeper.png', ); expect(profile?.landmarks[0]?.imageSrc).toBe( '/assets/custom-world/landmark-clocktower.png', ); expect( profile?.sceneChapterBlueprints?.[0]?.acts[0]?.backgroundImageSrc, ).toBe('/assets/custom-world/act-clocktower-opening.png'); }); it('近似无损保留编辑态和运行态结构字段', () => { const profile = normalizeCustomWorldProfileRecord({ name: '星砂废都', settingText: '坠星沙海与废都钟楼', attributeSchema: { id: 'schema-stardust', worldId: 'world-stardust', schemaVersion: 1, generatedFrom: { worldType: 'CUSTOM', worldName: '星砂废都', settingSummary: '坠星沙海与废都钟楼', tone: '苍凉', conflictCore: '旧约与星砂潮汐冲突', }, slots: [ { slotId: 'axis_a', name: '星砂共鸣' }, { slotId: 'axis_b', name: '废都步法' }, { slotId: 'axis_c', name: '钟楼感知' }, { slotId: 'axis_d', name: '旧约心火' }, { slotId: 'axis_e', name: '尘缘牵引' }, { slotId: 'axis_f', name: '潮汐玄息' }, ], }, camp: { id: 'camp-star-waste', name: '废都营地', description: '钟楼阴影下的临时营地。', narrativeResidues: [ { id: 'residue-camp-1', summary: '营地火盆里混着星砂。', }, ], }, playableNpcs: [ { id: 'playable-shamian', name: '砂眠', title: '废都引路人', role: '主角代理', skills: [ { id: 'skill-star-step', name: '星砂步', summary: '踏砂突进。', style: 'mobility', actionPreviewConfig: { folder: 'characters/shamian', prefix: 'skill_', frames: 8, basePath: '/assets/custom-world/shamian/skill', previewVideoPath: '/assets/custom-world/shamian/skill.mp4', file: 'skill.png', }, }, ], initialItems: [ { id: 'item-sand-compass', name: '星砂罗盘', category: '专属物品', quantity: 1, rarity: 'rare', description: '能指出旧约埋藏方向。', tags: ['旧约'], iconSrc: '/assets/custom-world/items/sand-compass.png', }, ], attributeProfile: { schemaId: 'schema-stardust', values: { axis_a: 8, axis_b: 7 }, topTraits: ['星砂共鸣'], evidence: [ { slotId: 'axis_a', reason: '能听见星砂潮汐。' }, ], }, }, ], storyNpcs: [ { id: 'story-clock-keeper', name: '钟守', title: '钟楼守夜者', role: '第一幕主NPC', attributeProfile: { schemaId: 'schema-stardust', values: { axis_c: 9 }, topTraits: ['钟楼感知'], evidence: [ { slotId: 'axis_c', reason: '能辨认旧铃回声。' }, ], }, }, ], landmarks: [ { id: 'landmark-clocktower', name: '坠星钟楼', description: '半截钟楼被星砂埋住。', visualDescription: '钟楼外墙布满蓝白星砂结晶。', narrativeResidues: [ { id: 'residue-clocktower-1', summary: '钟楼第十三声铃响仍未散去。', }, ], }, ], sceneChapterBlueprints: [ { id: 'scene-chapter-clocktower', sceneId: 'landmark-clocktower', title: '钟楼第一夜', acts: [ { id: 'act-clocktower-opening', sceneId: 'landmark-clocktower', title: '第一幕', summary: '砂眠带玩家进入坠星钟楼。', backgroundAssetId: 'asset-act-clocktower-opening', backgroundImageSrc: '/assets/custom-world/act-clocktower-opening.png', eventDescription: '钟楼旧铃忽然自鸣。', linkedThreadIds: ['thread-old-vow'], advanceRule: 'after_primary_contact', actGoal: '进入钟楼。', transitionHook: '星砂开始倒流。', }, ], }, ], }); expect(profile?.attributeSchema.id).toBe('schema-stardust'); expect(profile?.attributeSchema.slots[0]?.name).toBe('星砂共鸣'); expect(profile?.camp?.narrativeResidues?.[0]?.summary).toBe( '营地火盆里混着星砂。', ); expect( profile?.playableNpcs[0]?.skills[0]?.actionPreviewConfig ?.previewVideoPath, ).toBe('/assets/custom-world/shamian/skill.mp4'); expect(profile?.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe( '/assets/custom-world/items/sand-compass.png', ); expect(profile?.playableNpcs[0]?.attributeProfile?.values.axis_a).toBe(8); expect(profile?.storyNpcs[0]?.attributeProfile?.topTraits).toContain( '钟楼感知', ); expect(profile?.landmarks[0]?.visualDescription).toBe( '钟楼外墙布满蓝白星砂结晶。', ); expect(profile?.landmarks[0]?.narrativeResidues?.[0]?.summary).toBe( '钟楼第十三声铃响仍未散去。', ); const act = profile?.sceneChapterBlueprints?.[0]?.acts[0]; expect(act?.backgroundAssetId).toBe('asset-act-clocktower-opening'); expect(act?.eventDescription).toBe('钟楼旧铃忽然自鸣。'); expect(act?.linkedThreadIds).toEqual(['thread-old-vow']); expect(act?.actGoal).toBe('进入钟楼。'); expect(act?.transitionHook).toBe('星砂开始倒流。'); }); it('保留只有背景资产的场景幕,避免恢复详情时丢失场景 CG', () => { const profile = normalizeCustomWorldProfileRecord({ name: '星砂废都', settingText: '坠星沙海与废都钟楼', sceneChapterBlueprints: [ { id: 'scene-chapter-clocktower', sceneId: 'landmark-clocktower', title: '钟楼第一夜', acts: [ { id: 'act-background-only', sceneId: 'landmark-clocktower', backgroundAssetId: 'asset-background-only', backgroundImageSrc: '/assets/custom-world/background-only.png', backgroundPromptText: '坠星钟楼被蓝白星砂照亮。', }, ], }, ], }); const act = profile?.sceneChapterBlueprints?.[0]?.acts[0]; expect(act?.id).toBe('act-background-only'); expect(act?.backgroundImageSrc).toBe( '/assets/custom-world/background-only.png', ); expect(act?.backgroundAssetId).toBe('asset-background-only'); expect(act?.backgroundPromptText).toBe('坠星钟楼被蓝白星砂照亮。'); }); });