This commit is contained in:
281
src/services/customWorld.test.ts
Normal file
281
src/services/customWorld.test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels';
|
||||
import { getCurrencyName } from '../data/economy';
|
||||
import { WorldType } from '../types';
|
||||
import { normalizeCustomWorldProfile } from './customWorld';
|
||||
|
||||
describe('normalizeCustomWorldProfile', () => {
|
||||
it('forces NPC backstory chapter thresholds to match shared affinity levels', () => {
|
||||
const rawChapterThresholds = [20, 40, 65, 85];
|
||||
const rawProfile = {
|
||||
name: '裂谷边城',
|
||||
playableNpcs: [
|
||||
{
|
||||
name: '沈砺',
|
||||
title: '灰炬向导',
|
||||
role: '向导',
|
||||
description: '常年带人穿过裂谷旧道。',
|
||||
backstory: '曾在塌桥夜里失去整支同行队伍。',
|
||||
personality: '谨慎寡言,却记得每一道风口。',
|
||||
motivation: '想查清旧道频繁异变的根源。',
|
||||
combatStyle: '短弓牵制后再逼近补刀。',
|
||||
initialAffinity: 18,
|
||||
relationshipHooks: ['带路', '旧案'],
|
||||
tags: ['裂谷', '向导'],
|
||||
backstoryReveal: {
|
||||
publicSummary: '他只说自己熟悉旧道。',
|
||||
chapters: rawChapterThresholds.map((affinityRequired, index) => ({
|
||||
id: `playable-${index + 1}`,
|
||||
title: `章节${index + 1}`,
|
||||
affinityRequired,
|
||||
teaser: `提示${index + 1}`,
|
||||
content: `内容${index + 1}`,
|
||||
contextSnippet: `摘要${index + 1}`,
|
||||
})),
|
||||
},
|
||||
skills: [
|
||||
{ name: '灰炬起手', summary: '先以火光扰乱视线。', style: '起手压制' },
|
||||
{ name: '窄道游移', summary: '借地形不断换位牵制。', style: '机动周旋' },
|
||||
{ name: '崖风绝射', summary: '抓住破绽给出终结一箭。', style: '爆发终结' },
|
||||
],
|
||||
initialItems: [
|
||||
{ name: '旧道短弓', category: '武器', quantity: 1, rarity: 'rare', description: '磨损严重却极趁手。', tags: ['裂谷'] },
|
||||
{ name: '裂谷补给', category: '消耗品', quantity: 2, rarity: 'uncommon', description: '防风与止血一并备齐。', tags: ['补给'] },
|
||||
{ name: '断绳铜哨', category: '专属物品', quantity: 1, rarity: 'rare', description: '那场事故后仅存的信物。', tags: ['旧案'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
storyNpcs: [
|
||||
{
|
||||
name: '裂谷巡哨蛛',
|
||||
title: '巡哨怪',
|
||||
role: '怪物哨兵',
|
||||
description: '伏在岩壁缝间监视往来活物。',
|
||||
backstory: '长期吞食矿脉异潮后逐渐拥有巡猎习性。',
|
||||
personality: '极度警觉,会反复试探猎物退路。',
|
||||
motivation: '守住巢穴上层不断扩大的裂口。',
|
||||
combatStyle: '吐丝封路,再借高处俯冲撕咬。',
|
||||
initialAffinity: -20,
|
||||
relationshipHooks: ['巢穴', '异潮'],
|
||||
tags: ['怪物', '裂谷'],
|
||||
backstoryReveal: {
|
||||
publicSummary: '它始终盘踞在峭壁阴影里。',
|
||||
chapters: rawChapterThresholds.map((affinityRequired, index) => ({
|
||||
id: `story-${index + 1}`,
|
||||
title: `章节${index + 1}`,
|
||||
affinityRequired,
|
||||
teaser: `怪物提示${index + 1}`,
|
||||
content: `怪物内容${index + 1}`,
|
||||
contextSnippet: `怪物摘要${index + 1}`,
|
||||
})),
|
||||
},
|
||||
skills: [
|
||||
{ name: '蛛丝封步', summary: '先缠住脚步再逼近。', style: '起手压制' },
|
||||
{ name: '壁缝换位', summary: '沿岩壁快速转移位置。', style: '机动周旋' },
|
||||
{ name: '坠崖扑杀', summary: '从高处俯冲撕裂目标。', style: '爆发终结' },
|
||||
],
|
||||
initialItems: [
|
||||
{ name: '硬化毒牙', category: '材料', quantity: 1, rarity: 'rare', description: '可提炼出刺激性毒液。', tags: ['怪物'] },
|
||||
{ name: '粘稠丝囊', category: '材料', quantity: 2, rarity: 'uncommon', description: '能用于制作束缚陷阱。', tags: ['巢穴'] },
|
||||
{ name: '矿潮节壳', category: '稀有品', quantity: 1, rarity: 'rare', description: '受异潮侵染后的外壳碎片。', tags: ['异潮'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
landmarks: [
|
||||
{
|
||||
name: '北侧塌桥',
|
||||
description: '横跨裂谷的旧桥只剩半截石拱。',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const profile = normalizeCustomWorldProfile(rawProfile, '玩家想要一个裂谷边城与怪物共存的世界。');
|
||||
|
||||
expect(
|
||||
profile.playableNpcs[0]?.backstoryReveal.chapters.map(
|
||||
(chapter) => chapter.affinityRequired,
|
||||
),
|
||||
).toEqual(AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS);
|
||||
expect(
|
||||
profile.storyNpcs[0]?.backstoryReveal.chapters.map(
|
||||
(chapter) => chapter.affinityRequired,
|
||||
),
|
||||
).toEqual(AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS);
|
||||
});
|
||||
|
||||
it('resolves landmark scene NPCs and relative connections into the final scene graph', () => {
|
||||
const rawProfile = {
|
||||
name: '裂界巡旅',
|
||||
playableNpcs: [
|
||||
{
|
||||
name: '岑舟',
|
||||
title: '裂界行脚',
|
||||
role: '引路人',
|
||||
description: '擅长在断层边缘辨路。',
|
||||
backstory: '长期在裂界边缘押送队伍。',
|
||||
personality: '稳重少言,但反应很快。',
|
||||
motivation: '想把几条旧通路重新串起来。',
|
||||
combatStyle: '短兵贴身后迅速换位。',
|
||||
initialAffinity: 18,
|
||||
relationshipHooks: ['带路', '断层'],
|
||||
tags: ['裂界', '向导'],
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
],
|
||||
storyNpcs: [
|
||||
{
|
||||
name: '梁砺',
|
||||
title: '桥索修补匠',
|
||||
role: '修桥人',
|
||||
description: '守着断桥口修缮索道。',
|
||||
backstory: '曾在崩桥夜里救下半队人。',
|
||||
personality: '谨慎,习惯先看绳结再说话。',
|
||||
motivation: '想守住最后几条安全通路。',
|
||||
combatStyle: '铁钩牵制后贴近补击。',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['断桥', '索道'],
|
||||
tags: ['桥', '工匠'],
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
{
|
||||
name: '苏雾',
|
||||
title: '雾港采录者',
|
||||
role: '记录员',
|
||||
description: '在雾港整理各路来客口供。',
|
||||
backstory: '长期记录裂雾里消失的队伍名单。',
|
||||
personality: '敏感细致,总在核对细节。',
|
||||
motivation: '查清名单上重复出现的名字。',
|
||||
combatStyle: '保持距离,借器物扰乱节奏。',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['雾港', '名单'],
|
||||
tags: ['港口', '记录'],
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
{
|
||||
name: '顾岚',
|
||||
title: '界崖巡哨',
|
||||
role: '巡哨',
|
||||
description: '沿着崖线巡查异动和回声。',
|
||||
backstory: '常年住在界崖边的哨点里。',
|
||||
personality: '警觉直接,不喜欢绕弯。',
|
||||
motivation: '找出最近总在夜里响起的回声来源。',
|
||||
combatStyle: '长兵抢先压住身位。',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['巡查', '崖线'],
|
||||
tags: ['哨点', '崖线'],
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
{
|
||||
name: '闻砂',
|
||||
title: '砂塔守更人',
|
||||
role: '守更人',
|
||||
description: '夜里守着砂塔边的旧灯火。',
|
||||
backstory: '见过太多从塔下走失的人。',
|
||||
personality: '冷静克制,习惯留后手。',
|
||||
motivation: '想确认旧塔下方的回响是否重新苏醒。',
|
||||
combatStyle: '借高差压制后再收拢路线。',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['守夜', '砂塔'],
|
||||
tags: ['砂塔', '旧灯'],
|
||||
skills: [],
|
||||
initialItems: [],
|
||||
},
|
||||
],
|
||||
landmarks: [
|
||||
{
|
||||
name: '北侧塌桥',
|
||||
description: '断桥上方还残留着旧索道。',
|
||||
sceneNpcNames: ['梁砺'],
|
||||
connections: [
|
||||
{
|
||||
targetLandmarkName: '雾潮码头',
|
||||
relativePosition: 'south',
|
||||
summary: '顺着残桥往南下坡可到雾港。',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '雾潮码头',
|
||||
description: '潮雾会把来路和去路都遮住一半。',
|
||||
sceneNpcNames: ['苏雾', '顾岚'],
|
||||
connections: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const profile = normalizeCustomWorldProfile(
|
||||
rawProfile,
|
||||
'玩家想要一个围绕裂界断桥与雾港巡旅展开的世界。',
|
||||
);
|
||||
|
||||
expect(profile.landmarks).toHaveLength(2);
|
||||
expect(profile.landmarks[0]?.sceneNpcIds).toHaveLength(3);
|
||||
expect(profile.landmarks[1]?.sceneNpcIds).toHaveLength(3);
|
||||
expect(profile.landmarks[0]?.connections[0]?.targetLandmarkId).toBe(
|
||||
profile.landmarks[1]?.id,
|
||||
);
|
||||
expect(profile.landmarks[1]?.connections.some(
|
||||
(connection) => connection.targetLandmarkId === profile.landmarks[0]?.id,
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('compiles and preserves owned setting layers for runtime consumption', () => {
|
||||
const profile = normalizeCustomWorldProfile(
|
||||
{
|
||||
name: '雾潮港',
|
||||
summary: '被潮灾旧闻反复撕开的边港。',
|
||||
tone: '潮湿、迷雾、压抑',
|
||||
playerGoal: '查清港区失踪名单为何重复出现',
|
||||
templateWorldType: WorldType.WUXIA,
|
||||
ownedSettingLayers: {
|
||||
ruleProfile: {
|
||||
resourceLabels: {
|
||||
hp: '潮命',
|
||||
mp: '潮息',
|
||||
maxHp: '潮命上限',
|
||||
maxMp: '潮息上限',
|
||||
damage: '潮势',
|
||||
guard: '潮护',
|
||||
range: '潮距',
|
||||
cooldown: '回潮',
|
||||
manaCost: '潮息消耗',
|
||||
currency: '雾银',
|
||||
},
|
||||
economyProfile: {
|
||||
initialCurrency: 188,
|
||||
},
|
||||
},
|
||||
semanticAnchor: {
|
||||
genreSignals: ['海岸悬疑'],
|
||||
conflictForms: ['追查失踪'],
|
||||
institutionTypes: ['港务'],
|
||||
tabooTypes: ['回潮夜'],
|
||||
carrierTypes: ['航图'],
|
||||
forceSystemTypes: ['潮汐'],
|
||||
atmosphereTags: ['迷雾'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'玩家想要一个围绕迷雾港区与潮灾旧闻展开的世界。',
|
||||
);
|
||||
|
||||
expect(profile.ownedSettingLayers?.ruleProfile.resourceLabels.currency).toBe(
|
||||
'雾银',
|
||||
);
|
||||
expect(profile.ownedSettingLayers?.ruleProfile.economyProfile.initialCurrency).toBe(
|
||||
188,
|
||||
);
|
||||
expect(getCurrencyName(WorldType.CUSTOM, profile)).toBe('雾银');
|
||||
expect(
|
||||
profile.ownedSettingLayers?.compatibilityProfile?.legacyTemplateWorldType,
|
||||
).toBe(WorldType.WUXIA);
|
||||
expect(
|
||||
profile.ownedSettingLayers?.referenceProfile.creatureArchetypes.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user