import assert from 'node:assert/strict'; import test from 'node:test'; import { generateCustomWorldEntity } from './customWorldEntityGenerationService.js'; import type { UpstreamLlmClient } from './llmClient.js'; function createProfile() { return { name: '裂潮边城', settingText: '裂潮重新逼近边城,旧封桥令也被重新翻出。', summary: '一座在裂潮与旧案之间摇摇欲坠的边城。', tone: '紧绷、克制、暗流涌动', playerGoal: '查清封桥旧令背后的真正操盘者', playableNpcs: [ { id: 'playable-1', name: '沈砺', title: '灰炬向导', role: '边路同行者', description: '熟悉裂潮边路的向导。', visualDescription: '灰斗篷和旧路标是他最显眼的识别点。', actionDescription: '先试探风向,再用短弓牵制。', sceneVisualDescription: '他常在旧边路哨点出现。', backstory: '曾在旧撤离线里失去整支同行队。', personality: '谨慎寡言。', motivation: '想查清旧撤离线再次失控的原因。', combatStyle: '短弓牵制后贴近补刀。', initialAffinity: 18, relationshipHooks: ['旧撤离线'], tags: ['裂潮', '向导'], }, ], storyNpcs: [ { id: 'story-1', name: '梁砺', title: '断桥巡守', role: '巡守', description: '守着旧桥与哨火的人。', visualDescription: '披着旧制巡守外袍,枪柄磨损很重。', actionDescription: '先立枪封路,再逼近压线。', sceneVisualDescription: '多出现在断桥和潮湿石阶附近。', backstory: '旧案爆发时,他是最后一个封桥的人。', personality: '直接、警觉。', motivation: '不想再让封桥旧案被人利用。', combatStyle: '长枪压线。', initialAffinity: 6, relationshipHooks: ['断桥'], tags: ['巡守'], }, ], landmarks: [ { id: 'landmark-1', name: '旧潮栈桥', description: '裂潮来时最先响起铁索声的旧栈桥。', visualDescription: '铁索、旧桩和盐雾一起压在栈桥上。', dangerLevel: 'medium', sceneNpcIds: ['story-1'], connections: [], }, ], }; } test('generateCustomWorldEntity returns role-side visual descriptions from the same model response', async () => { const llmClient = { requestMessageContent: async () => JSON.stringify({ playableNpc: { name: '顾潮音', title: '潮港校灯人', role: '边港同行者', description: '在港区高处替玩家校正风向与路标的人。', visualDescription: '深蓝防潮外套压着风痕,腰侧悬着校灯尺和短刃,整个人像常年站在高处看潮线的人。', actionDescription: '先借高处观察和校灯信号牵制敌人,再突然贴近切断退路。', sceneVisualDescription: '他第一次出现的高台边缘挂着潮湿风旗,脚下是被盐雾浸白的木板和仍亮着的旧校灯。', backstory: '曾负责港区夜航校灯,后被卷进旧案。', personality: '沉稳、寡言、观察细。', motivation: '想在港区秩序彻底失控前找到还能守住的线。', combatStyle: '高差观察后快速切入。', initialAffinity: 24, relationshipHooks: ['夜航校灯', '旧港案'], tags: ['港区', '校灯'], publicSummary: '港区里很少有人比他更熟悉夜里的风向。', chapterTeasers: ['他盯风向比盯人更久。', '旧港案在他身上没过去。', '他一直在等某个信号。', '他还藏着最后一次校灯记录。'], chapterContents: ['他总先校风向。', '旧港案改变了他的站位。', '他真正守的是港区里还没断的线。', '最后那份校灯记录能指向操盘者。'], skills: [ { name: '校灯试探', summary: '先用灯信号试探敌我位置。', style: '起手压制' }, { name: '斜坡切入', summary: '借高差快速贴近改线。', style: '机动周旋' }, { name: '潮线封口', summary: '看准潮线后一口气断掉退路。', style: '爆发终结' }, ], initialItems: [ { name: '校灯尺', category: '武器', quantity: 1, rarity: 'rare', description: '兼具校灯与近战功能。', tags: ['港区'] }, { name: '旧港图片', category: '专属物品', quantity: 1, rarity: 'rare', description: '记着他自己的旧线路。', tags: ['旧案'] }, { name: '潮雾止血包', category: '消耗品', quantity: 2, rarity: 'uncommon', description: '港区常备。', tags: ['补给'] }, ], }, }), } as UpstreamLlmClient; const result = await generateCustomWorldEntity(llmClient, { profile: createProfile(), kind: 'playable', }); assert.equal(result.kind, 'playable'); assert.equal( result.entity.visualDescription, '深蓝防潮外套压着风痕,腰侧悬着校灯尺和短刃,整个人像常年站在高处看潮线的人。', ); assert.equal( result.entity.actionDescription, '先借高处观察和校灯信号牵制敌人,再突然贴近切断退路。', ); assert.equal( result.entity.sceneVisualDescription, '他第一次出现的高台边缘挂着潮湿风旗,脚下是被盐雾浸白的木板和仍亮着的旧校灯。', ); }); test('generateCustomWorldEntity returns landmark visual descriptions from the same model response', async () => { const llmClient = { requestMessageContent: async () => JSON.stringify({ landmark: { name: '回潮观测台', description: '能俯瞰旧港和裂潮边缘的新观测点。', visualDescription: '观测台立在湿冷高处,铁质风标、旧灯架和被潮气侵白的石阶一起构成了压迫感很强的前景。', dangerLevel: 'high', sceneNpcNames: ['梁砺'], connections: [ { targetLandmarkName: '旧潮栈桥', relativePosition: 'forward', summary: '沿风雨走廊可直接回到旧潮栈桥', }, ], }, }), } as UpstreamLlmClient; const result = await generateCustomWorldEntity(llmClient, { profile: createProfile(), kind: 'landmark', }); assert.equal(result.kind, 'landmark'); assert.equal( result.entity.visualDescription, '观测台立在湿冷高处,铁质风标、旧灯架和被潮气侵白的石阶一起构成了压迫感很强的前景。', ); });