Files
Genarrative/src/data/characterPresets.customWorld.test.ts

287 lines
9.8 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import { buildExpandedCustomWorldProfile } from '../services/customWorldBuilder';
import { AnimationState } from '../types';
import {
buildCustomWorldPlayableCharacters,
buildCustomWorldRuntimeCharacters,
getCharacterById,
resolveEncounterRecruitCharacter,
setRuntimeCharacterOverrides,
} from './characterPresets';
import { setRuntimeCustomWorldProfile } from './customWorldRuntime';
function createRole(index: number) {
return {
name: `角色${index + 1}`,
title: `头衔${index + 1}`,
role: `身份${index + 1}`,
description: `角色描述${index + 1}`,
backstory: `角色背景${index + 1}`,
personality: `角色性格${index + 1}`,
motivation: `角色动机${index + 1}`,
combatStyle: `角色战斗风格${index + 1}`,
initialAffinity: 18,
relationshipHooks: [`关系${index + 1}`],
tags: [`标签${index + 1}`],
backstoryReveal: {
publicSummary: `公开背景${index + 1}`,
chapters: [
{
id: `surface-${index + 1}`,
title: '表层来意',
affinityRequired: 10,
teaser: `提示${index + 1}-1`,
content: `内容${index + 1}-1`,
contextSnippet: `摘要${index + 1}-1`,
},
{
id: `scar-${index + 1}`,
title: '旧事裂痕',
affinityRequired: 30,
teaser: `提示${index + 1}-2`,
content: `内容${index + 1}-2`,
contextSnippet: `摘要${index + 1}-2`,
},
{
id: `hidden-${index + 1}`,
title: '隐藏执念',
affinityRequired: 55,
teaser: `提示${index + 1}-3`,
content: `内容${index + 1}-3`,
contextSnippet: `摘要${index + 1}-3`,
},
{
id: `final-${index + 1}`,
title: '最终底牌',
affinityRequired: 80,
teaser: `提示${index + 1}-4`,
content: `内容${index + 1}-4`,
contextSnippet: `摘要${index + 1}-4`,
},
],
},
skills: [
{ name: `技能${index + 1}-1`, summary: '技能摘要1', style: '起手压制' },
{ name: `技能${index + 1}-2`, summary: '技能摘要2', style: '机动周旋' },
{ name: `技能${index + 1}-3`, summary: '技能摘要3', style: '爆发终结' },
],
initialItems: [
{
name: `武器${index + 1}`,
category: '武器',
quantity: 1,
rarity: 'rare' as const,
description: '武器描述',
tags: ['武器标签'],
},
{
name: `补给${index + 1}`,
category: '消耗品',
quantity: 2,
rarity: 'uncommon' as const,
description: '补给描述',
tags: ['补给标签'],
},
{
name: `信物${index + 1}`,
category: '专属物品',
quantity: 1,
rarity: 'rare' as const,
description: '信物描述',
tags: ['信物标签'],
},
],
};
}
describe('characterPresets custom world runtime characters', () => {
afterEach(() => {
setRuntimeCharacterOverrides(null);
setRuntimeCustomWorldProfile(null);
});
it('hydrates story npcs into runtime characters and preserves custom dossiers', () => {
const profile = buildExpandedCustomWorldProfile(
{
name: '裂潮边城',
subtitle: '潮痕未褪',
summary: '一座围绕潮路、断桥和夜港旧案展开的世界。',
tone: '潮湿、压抑、克制',
playerGoal: '查清夜港失踪案和潮路背后的势力牵连。',
templateWorldType: 'WUXIA',
playableNpcs: Array.from({ length: 5 }, (_, index) =>
createRole(index),
),
storyNpcs: [
{
...createRole(10),
name: '沈雾',
title: '潮路领航人',
role: '夜港向导',
description: '熟悉潮路暗栈与旧渡的人。',
backstory: '曾在断桥坠潮夜里失去整队同伴。',
personality: '谨慎冷静,先观察再表态。',
motivation: '想把失踪航线重新找出来。',
combatStyle: '短刀试探后再借地形逼近。',
initialAffinity: 12,
relationshipHooks: ['断桥旧案', '夜港潮路'],
tags: ['码头', '潮路', '短刀'],
imageSrc: '/custom/npcs/shenwu.png',
generatedVisualAssetId: 'visual-custom-shenwu',
generatedAnimationSetId: 'animation-set-custom-shenwu',
animationMap: {
[AnimationState.IDLE]: {
folder: 'idle',
prefix: 'frame',
frames: 8,
startFrame: 1,
extension: 'png',
basePath:
'/generated-animations/custom-shenwu/animation-set-custom-shenwu/idle',
},
[AnimationState.ATTACK]: {
folder: 'attack',
prefix: 'frame',
frames: 8,
startFrame: 1,
extension: 'png',
basePath:
'/generated-animations/custom-shenwu/animation-set-custom-shenwu/attack',
},
},
visual: {
race: 'human',
bodyColor: 'blue',
headIndex: 2,
hairColorIndex: 3,
hairStyleFrame: 5,
facialHairEnabled: false,
facialHairColorIndex: 1,
facialHairStyleFrame: 0,
mainHand: {
type: 'melee',
file: 'dagger.png',
frameIndex: 4,
},
},
},
{
...createRole(11),
name: '陆沉',
title: '断桥守更',
role: '守桥人',
description: '夜里守着断桥口旧灯火的人。',
},
{
...createRole(12),
name: '顾潮',
title: '潮册记录员',
role: '账房记录员',
description: '在潮账房里整理失踪名册的人。',
},
],
landmarks: [
{
name: '夜港旧栈',
description: '潮雾和旧木桥把视线切成断续几段。',
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
connections: [
{
targetLandmarkName: '断桥外沿',
relativePosition: 'forward',
summary: '顺着潮路继续前压就是断桥外沿。',
},
],
},
{
name: '断桥外沿',
description: '旧桥断口还挂着潮湿残旗。',
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
connections: [
{
targetLandmarkName: '夜港旧栈',
relativePosition: 'back',
summary: '沿旧潮路退回夜港旧栈。',
},
],
},
],
},
'玩家想要一个围绕夜港潮路与断桥旧案展开的世界。',
);
setRuntimeCustomWorldProfile(profile);
const runtimeCharacters = buildCustomWorldRuntimeCharacters(profile);
setRuntimeCharacterOverrides(runtimeCharacters);
const storyRole = profile.storyNpcs[0];
expect(storyRole).toBeTruthy();
const storyCharacter = getCharacterById(storyRole!.id);
const runtimeStoryCharacter = runtimeCharacters.find(
(character) => character.id === storyRole!.id,
);
expect(storyCharacter).toBeTruthy();
expect(runtimeStoryCharacter).toBeTruthy();
expect(storyCharacter?.name).toBe('沈雾');
expect(storyCharacter?.title).toBe('潮路领航人');
expect(storyCharacter?.backstory).toContain('断桥坠潮夜');
expect(storyCharacter?.skills[0]?.name).toBe('技能11-1');
expect(storyCharacter?.portrait).toBe('/custom/npcs/shenwu.png');
expect(storyCharacter?.generatedVisualAssetId).toBe(
'visual-custom-shenwu',
);
expect(storyCharacter?.generatedAnimationSetId).toBe(
'animation-set-custom-shenwu',
);
expect(storyCharacter?.animationMap?.[AnimationState.IDLE]?.basePath).toBe(
'/generated-animations/custom-shenwu/animation-set-custom-shenwu/idle',
);
expect(storyCharacter?.visual).toEqual(storyRole?.visual);
expect(storyCharacter?.groundOffsetY).toBe(22);
const recruitCharacter = resolveEncounterRecruitCharacter({
characterId: storyRole!.id,
context: storyRole!.role,
npcName: storyRole!.name,
});
expect(recruitCharacter?.id).toBe(storyRole!.id);
expect(recruitCharacter?.name).toBe('沈雾');
});
it('uses draft playable role image directly before generated animations exist', () => {
const profile = buildExpandedCustomWorldProfile(
{
name: '潮雾列岛',
subtitle: '灯塔未眠',
summary: '围绕潮雾、灯塔和失踪航路展开的世界。',
tone: '冷峻、潮湿、悬疑',
playerGoal: '找到灯塔失踪航路。',
templateWorldType: 'WUXIA',
playableNpcs: [
{
...createRole(0),
id: 'playable-lighthouse-keeper',
imageSrc: '/generated-characters/lighthouse-keeper/portrait.png',
generatedVisualAssetId: 'assetobj-lighthouse-keeper',
generatedAnimationSetId: undefined,
animationMap: undefined,
},
],
},
'玩家想测试灯塔守望者草稿。',
);
const [playableCharacter] = buildCustomWorldPlayableCharacters(profile);
expect(playableCharacter?.portrait).toBe(
'/generated-characters/lighthouse-keeper/portrait.png',
);
expect(playableCharacter?.avatar).toBe(
'/generated-characters/lighthouse-keeper/portrait.png',
);
expect(playableCharacter?.animationMap).toBeUndefined();
});
});