import { describe, expect, it } from 'vitest'; import { buildChapterQuestForScene } from '../../data/questFlow'; import { AnimationState, type GameState, WorldType } from '../../types'; import { advanceChapterState, resolveCurrentChapterState } from './chapterDirector'; function createState(signalCount: number): GameState { return { worldType: null, customWorldProfile: null, playerCharacter: null, runtimeStats: { playTimeMs: 0, lastPlayTickAt: null, hostileNpcsDefeated: 0, questsAccepted: 0, itemsUsed: 0, scenesTraveled: 0, }, currentScene: 'Story', storyHistory: [], storyEngineMemory: { discoveredFactIds: [], inferredFactIds: [], activeThreadIds: ['thread-1'], resolvedScarIds: [], recentCarrierIds: [], recentSignalIds: Array.from({ length: signalCount }, (_, index) => `signal-${index + 1}`), recentCompanionReactions: [], currentChapter: null, currentJourneyBeatId: null, companionArcStates: [], worldMutations: [], chronicle: [], factionTensionStates: [], currentCampEvent: null, currentSetpieceDirective: null, continueGameDigest: null, }, chapterState: null, characterChats: {}, animationState: AnimationState.IDLE, currentEncounter: null, npcInteractionActive: false, currentScenePreset: null, sceneHostileNpcs: [], playerX: 0, playerOffsetY: 0, playerFacing: 'right', playerActionMode: 'idle', scrollWorld: false, inBattle: false, playerHp: 0, playerMaxHp: 0, playerMana: 0, playerMaxMana: 0, playerSkillCooldowns: {}, activeBuildBuffs: [], activeCombatEffects: [], playerCurrency: 0, playerInventory: [], playerEquipment: { weapon: null, armor: null, relic: null }, npcStates: {}, quests: [], roster: [], companions: [], currentBattleNpcId: null, currentNpcBattleMode: null, currentNpcBattleOutcome: null, sparReturnEncounter: null, sparPlayerHpBefore: null, sparPlayerMaxHpBefore: null, sparStoryHistoryBefore: null, }; } function createSceneChapterState() { const quest = buildChapterQuestForScene({ scene: { id: 'scene-court', name: '宫苑内庭', description: '回廊深处静得过分。', npcs: [ { id: 'npc-maid', name: '旧宫侍女', description: '她总知道哪条回廊最近不该过去。', avatar: '侍', role: '宫人', hostile: false, }, { id: 'hostile-shadow', name: '旧宫戍影', description: '巡行在回廊里的敌影。', avatar: '戍', role: '敌对角色', monsterPresetId: 'monster-11', hostile: true, }, ], treasureHints: ['回廊暗格里的香囊'], }, worldType: WorldType.WUXIA, }); if (!quest) { throw new Error('Expected chapter quest'); } return { ...createState(0), currentScenePreset: { id: 'scene-court', name: '宫苑内庭', description: '回廊深处静得过分。', imageSrc: '/scene.png', treasureHints: ['回廊暗格里的香囊'], npcs: [], }, quests: [quest], } satisfies GameState; } describe('chapterDirector', () => { it('resolves chapter stages from signal intensity', () => { expect(resolveCurrentChapterState({ state: createState(1) }).stage).toBe('opening'); expect(resolveCurrentChapterState({ state: createState(4) }).stage).toBe('expansion'); expect(resolveCurrentChapterState({ state: createState(10) }).stage).toBe('climax'); }); it('keeps chapter id stable when stage and theme do not change', () => { const previous = resolveCurrentChapterState({ state: createState(4) }); const next = advanceChapterState({ previousChapter: previous, nextChapter: resolveCurrentChapterState({ state: createState(4) }), }); expect(next.id).toBe(previous.id); }); it('binds the current chapter to the current scene chapter quest', () => { const openingState = createSceneChapterState(); const openingChapter = resolveCurrentChapterState({ state: openingState }); expect(openingChapter.id).toBe('chapter:scene:scene-court'); expect(openingChapter.sceneId).toBe('scene-court'); expect(openingChapter.chapterQuestId).toBe('quest:chapter:scene-court'); expect(openingChapter.stage).toBe('opening'); const turningState: GameState = { ...openingState, quests: [ { ...openingState.quests[0]!, steps: openingState.quests[0]!.steps?.map((step) => step.id === 'step_scene_opening' ? { ...step, progress: step.requiredCount } : step.id === 'step_scene_pressure' ? { ...step, progress: step.requiredCount } : step, ), activeStepId: 'step_scene_turning', }, ], }; expect(resolveCurrentChapterState({ state: turningState }).stage).toBe('turning_point'); const climaxState: GameState = { ...turningState, quests: [ { ...turningState.quests[0]!, steps: turningState.quests[0]!.steps?.map((step) => ({ ...step, progress: step.requiredCount, })), activeStepId: null, status: 'ready_to_turn_in', }, ], }; expect(resolveCurrentChapterState({ state: climaxState }).stage).toBe('climax'); const aftermathState: GameState = { ...climaxState, quests: [ { ...climaxState.quests[0]!, status: 'turned_in', }, ], }; expect(resolveCurrentChapterState({ state: aftermathState }).stage).toBe('aftermath'); }); });