287 lines
9.8 KiB
TypeScript
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();
|
|
});
|
|
});
|