Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-08 11:58:47 +08:00
parent 9d2fc9e4b8
commit bd9fdcbe31
170 changed files with 18259 additions and 1049 deletions

View File

@@ -0,0 +1,231 @@
import { afterEach, describe, expect, it } from 'vitest';
import { buildExpandedCustomWorldProfile } from '../services/customWorldBuilder';
import {
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),
templateCharacterId:
index === 0
? 'sword-princess'
: index === 1
? 'archer-hero'
: index === 2
? 'girl-hero'
: index === 3
? 'punch-hero'
: 'fighter-4',
})),
storyNpcs: [
{
...createRole(10),
name: '沈雾',
title: '潮路领航人',
role: '夜港向导',
description: '熟悉潮路暗栈与旧渡的人。',
backstory: '曾在断桥坠潮夜里失去整队同伴。',
personality: '谨慎冷静,先观察再表态。',
motivation: '想把失踪航线重新找出来。',
combatStyle: '短刀试探后再借地形逼近。',
initialAffinity: 12,
relationshipHooks: ['断桥旧案', '夜港潮路'],
tags: ['码头', '潮路', '短刀'],
imageSrc: '/custom/npcs/shenwu.png',
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: '潮雾和旧木桥把视线切成断续几段。',
dangerLevel: 'medium',
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
connections: [
{
targetLandmarkName: '断桥外沿',
relativePosition: 'forward',
summary: '顺着潮路继续前压就是断桥外沿。',
},
],
},
{
name: '断桥外沿',
description: '旧桥断口还挂着潮湿残旗。',
dangerLevel: 'high',
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?.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('沈雾');
});
});