import { describe, expect, it } from 'vitest'; import type { QuestLogEntry, QuestStep, ScenePresetInfo } from '../types'; import { WorldType } from '../types'; import { applyQuestProgressFromHostileNpcDefeat, applyQuestProgressFromNpcTalk, buildChapterQuestForScene, buildQuestForEncounter, isQuestReadyToClaim, normalizeQuestLogEntries, } from './questFlow'; const TEST_SCENE = { id: 'forest_path', name: 'Forest Path', description: 'A narrow trail with fresh claw marks.', npcs: [ { id: 'hostile-wolf-alpha', name: '狼王', description: 'A hostile wolf alpha.', avatar: '狼', role: '敌对角色', monsterPresetId: 'wolf_alpha', initialAffinity: -40, hostile: true, }, ], treasureHints: [], } satisfies Pick< ScenePresetInfo, 'id' | 'name' | 'description' | 'npcs' | 'treasureHints' >; const CHAPTER_SCENE = { id: 'palace_court', name: '宫苑内庭', description: '回廊深处静得过分,花木修得齐整,却处处像埋着王庭旧案。', npcs: [ { id: 'npc-maid', name: '旧宫侍女', description: '她总知道哪条回廊最近不该过去。', avatar: '侍', role: '宫人', initialAffinity: 8, hostile: false, }, { id: 'hostile-guard', name: '旧宫戍影', description: '巡行在回廊深处的敌影。', avatar: '戍', role: '敌对角色', monsterPresetId: 'monster-11', initialAffinity: -40, hostile: true, }, ], treasureHints: ['回廊暗格里的香囊', '花圃石座下的旧金牌'], } satisfies Pick< ScenePresetInfo, 'id' | 'name' | 'description' | 'npcs' | 'treasureHints' >; const OVERRIDDEN_SCENE = { id: 'wuxia-palace-court', name: '宫苑内庭', description: '回廊深处静得过分,花木修得齐整,却处处像埋着王庭旧案。', npcs: [ { id: 'wuxia-npc-maid', name: '旧宫侍女', description: '嘴上说得少,却总知道哪条回廊最近不该过去。', avatar: '侍', role: '宫人', initialAffinity: 8, hostile: false, }, { id: 'hostile-guard', name: '旧宫戍影', description: '巡行在回廊深处的敌影。', avatar: '戍', role: '敌对角色', monsterPresetId: 'monster-11', initialAffinity: -40, hostile: true, }, ], treasureHints: ['回廊暗格里的香囊', '花圃石座下的旧金牌'], } satisfies Pick< ScenePresetInfo, 'id' | 'name' | 'description' | 'npcs' | 'treasureHints' >; function requireStep(quest: QuestLogEntry, stepId: string): QuestStep { const step = quest.steps?.find((item) => item.id === stepId); expect(step).toBeTruthy(); return step!; } describe('questFlow', () => { it('builds a staged quest contract for an encounter preview', () => { const quest = buildQuestForEncounter({ issuerNpcId: 'npc_scout', issuerNpcName: 'Scout Lin', roleText: 'tracker', scene: TEST_SCENE, worldType: WorldType.WUXIA, currentQuests: [], }); expect(quest).toBeTruthy(); expect(quest?.steps).toHaveLength(2); expect(quest?.objective.kind).toBe('defeat_hostile_npc'); expect(requireStep(quest!, 'step_primary').kind).toBe('defeat_hostile_npc'); expect(requireStep(quest!, 'step_report_back').kind).toBe('talk_to_npc'); expect(quest?.status).toBe('active'); expect(quest?.reward.experience).toBeGreaterThan(0); expect(quest?.rewardText).toContain('经验 +'); expect(quest?.reward.items[0]?.runtimeMetadata?.generationChannel).toBe( 'quest_reward', ); }); it('advances from primary objective to report-back step and then reward-ready', () => { const quest = buildQuestForEncounter({ issuerNpcId: 'npc_scout', issuerNpcName: 'Scout Lin', roleText: 'tracker', scene: TEST_SCENE, worldType: WorldType.WUXIA, currentQuests: [], }); expect(quest).toBeTruthy(); const afterBattle = applyQuestProgressFromHostileNpcDefeat( [quest!], TEST_SCENE.id, ['wolf_alpha'], )[0]; expect(afterBattle?.objective.kind).toBe('talk_to_npc'); expect(afterBattle?.status).toBe('active'); const afterReport = applyQuestProgressFromNpcTalk( [afterBattle!], 'npc_scout', )[0]; expect(afterReport?.status).toBe('ready_to_turn_in'); expect(isQuestReadyToClaim(afterReport!)).toBe(true); }); it('normalizes legacy single-objective quests into step-aware entries', () => { const normalized = normalizeQuestLogEntries([ { id: 'legacy', issuerNpcId: 'npc_scout', issuerNpcName: 'Scout Lin', sceneId: TEST_SCENE.id, title: 'Legacy Quest', description: 'Legacy description', summary: 'Legacy summary', objective: { kind: 'inspect_treasure', targetSceneId: TEST_SCENE.id, requiredCount: 1, }, progress: 1, status: 'completed', completionNotified: false, reward: { affinityBonus: 10, currency: 20, experience: 0, items: [], }, rewardText: 'Legacy reward text', }, ])[0]; expect(normalized?.steps).toHaveLength(1); expect(normalized?.steps?.[0]?.kind).toBe('inspect_treasure'); expect(normalized?.status).toBe('completed'); expect(normalized?.progress).toBe(1); }); it('builds a scene chapter quest that reuses staged quest steps', () => { const quest = buildChapterQuestForScene({ scene: CHAPTER_SCENE, worldType: WorldType.WUXIA, }); expect(quest).toBeTruthy(); expect(quest?.chapterId).toBe('chapter:scene:palace_court'); expect(quest?.sceneId).toBe('palace_court'); expect(quest?.reward.experience).toBeGreaterThan(0); expect(quest?.steps?.map((step) => step.kind)).toEqual([ 'talk_to_npc', 'defeat_hostile_npc', 'talk_to_npc', ]); }); it('uses sceneTaskDescription as first-entry chapter quest context', () => { const quest = buildChapterQuestForScene({ scene: CHAPTER_SCENE, worldType: WorldType.WUXIA, sceneChapterContext: { sceneTaskDescription: '首次进入宫苑内庭时,追查旧宫戍影为何守住回廊暗格。', actEventDescriptions: ['旧宫侍女先指出回廊暗格。'], primaryNpcName: '旧宫侍女', }, }); expect(quest?.description).toBe( '首次进入宫苑内庭时,追查旧宫戍影为何守住回廊暗格。', ); expect(quest?.narrativeBinding.dramaticNeed).toBe( '首次进入宫苑内庭时,追查旧宫戍影为何守住回廊暗格。', ); expect(quest?.narrativeBinding.playerHook).toContain('旧宫侍女'); expect(quest?.narrativeBinding.followupHooks).toContain( '旧宫侍女先指出回廊暗格。', ); }); it('lets scene chapter quests advance through npc talk and scene pressure steps', () => { const quest = buildChapterQuestForScene({ scene: CHAPTER_SCENE, worldType: WorldType.WUXIA, }); expect(quest).toBeTruthy(); const afterOpeningTalk = applyQuestProgressFromNpcTalk( [quest!], 'npc-maid', )[0]; expect(afterOpeningTalk?.objective.kind).toBe('defeat_hostile_npc'); const afterPressure = applyQuestProgressFromHostileNpcDefeat( [afterOpeningTalk!], CHAPTER_SCENE.id, ['monster-11'], )[0]; expect(afterPressure?.objective.kind).toBe('talk_to_npc'); const afterTurningTalk = applyQuestProgressFromNpcTalk( [afterPressure!], 'npc-maid', )[0]; expect(afterTurningTalk?.status).toBe('ready_to_turn_in'); expect(isQuestReadyToClaim(afterTurningTalk!)).toBe(true); }); it('uses scene chapter overrides to prefer investigation beats on key scenes', () => { const quest = buildChapterQuestForScene({ scene: OVERRIDDEN_SCENE, worldType: WorldType.WUXIA, }); expect(quest).toBeTruthy(); expect(quest?.title).toBe('查清内庭旧痕'); expect(requireStep(quest!, 'step_scene_pressure').kind).toBe( 'inspect_treasure', ); expect(requireStep(quest!, 'step_scene_pressure').title).toBe( '调查回廊暗格', ); expect(requireStep(quest!, 'step_scene_turning').title).toBe( '拿旧金牌去对问侍女', ); }); });