Files
Genarrative/src/services/customWorldCover.test.ts
2026-04-28 20:25:37 +08:00

150 lines
4.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { WorldType, type CustomWorldProfile } from '../types';
import { resolveCustomWorldCoverPresentation } from './customWorldCover';
function createBaseProfile(): CustomWorldProfile {
return {
id: 'custom-world-cover-test',
settingText: '潮雾群岛',
name: '潮雾群岛',
subtitle: '封面规则测试',
summary: '用于验证默认封面优先级。',
tone: '潮湿、压抑',
playerGoal: '查明旧航道真相。',
templateWorldType: WorldType.WUXIA,
compatibilityTemplateWorldType: WorldType.WUXIA,
majorFactions: [],
coreConflicts: [],
attributeSchema: {
id: 'schema:test',
worldId: 'CUSTOM',
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '潮雾群岛',
settingSummary: '封面规则测试',
tone: '潮湿、压抑',
conflictCore: '旧航道真相',
},
slots: [],
},
playableNpcs: [
{
id: 'playable-1',
name: '林潮',
title: '守潮人',
role: '可扮演角色',
description: '负责守住第一道进港口。',
backstory: '他在港口旧案里失去过同伴。',
personality: '谨慎克制。',
motivation: '想查清货船去向。',
combatStyle: '借地形换位。',
initialAffinity: 20,
relationshipHooks: ['旧案'],
tags: ['港口'],
backstoryReveal: {
publicSummary: '他对港口格外熟悉。',
chapters: [],
},
skills: [],
initialItems: [],
imageSrc: '/images/roles/linchao.webp',
},
],
storyNpcs: [],
items: [],
camp: {
id: 'camp-1',
name: '守夜营地',
description: '潮线后的临时据点。',
imageSrc: '/images/camp/camp.webp',
sceneNpcIds: [],
connections: [],
},
landmarks: [
{
id: 'landmark-1',
name: '潮汐码头',
description: '涨潮时会吞掉半截栈桥。',
imageSrc: '/images/landmark/docks.webp',
sceneNpcIds: [],
connections: [],
},
],
sceneChapterBlueprints: [
{
id: 'scene-chapter-1',
sceneId: 'landmark-1',
title: '潮汐码头',
summary: '第一章开局场景。',
sceneTaskDescription: '追查潮汐码头失踪案的第一条线索。',
linkedThreadIds: [],
linkedLandmarkIds: ['landmark-1'],
acts: [
{
id: 'act-1',
sceneId: 'landmark-1',
title: '雾里靠岸',
summary: '玩家第一次进入港口。',
stageCoverage: ['opening'],
backgroundImageSrc: '/images/scene/act-1.webp',
backgroundAssetId: 'asset-scene-act-1',
encounterNpcIds: [],
primaryNpcId: 'playable-1',
oppositeNpcId: 'playable-1',
eventDescription: '玩家第一次进入港口,发现涨潮后的异常痕迹。',
linkedThreadIds: [],
advanceRule: 'after_primary_contact',
actGoal: '拿到第一句真话。',
transitionHook: '下一幕将进入封锁区。',
},
],
},
],
};
}
describe('resolveCustomWorldCoverPresentation', () => {
it('优先使用开局场景第一幕图片作为默认封面底图', () => {
const profile = createBaseProfile();
const result = resolveCustomWorldCoverPresentation(profile);
expect(result.imageSrc).toBe('/images/scene/act-1.webp');
expect(result.renderMode).toBe('scene_with_roles');
expect(result.characterImageSrcs).toEqual(['/images/roles/linchao.webp']);
});
it('当第一幕图片缺失时按营地图与地标图顺序回退', () => {
const profile = createBaseProfile();
const firstSceneChapter = profile.sceneChapterBlueprints?.[0];
const firstSceneAct = firstSceneChapter?.acts[0];
if (!firstSceneChapter || !firstSceneAct) {
throw new Error('expected base profile to provide an opening scene chapter');
}
profile.sceneChapterBlueprints = [
{
...firstSceneChapter,
acts: [
{
...firstSceneAct,
backgroundImageSrc: null,
backgroundAssetId: null,
},
],
},
];
const fallbackToCamp = resolveCustomWorldCoverPresentation(profile);
expect(fallbackToCamp.imageSrc).toBe('/images/camp/camp.webp');
profile.camp = {
...profile.camp!,
imageSrc: '',
};
const fallbackToLandmark = resolveCustomWorldCoverPresentation(profile);
expect(fallbackToLandmark.imageSrc).toBe('/images/landmark/docks.webp');
});
});