Files
Genarrative/src/data/customWorldLibrary.test.ts
2026-05-22 03:14:11 +08:00

483 lines
17 KiB
TypeScript

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('坠星钟楼被蓝白星砂照亮。');
});
});