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); }); });