import type { SceneNode, Choice, EngineEvent } from '../types' import { SceneManager } from './SceneManager' import { VideoManager } from './VideoManager' import { StateManager } from './StateManager' import { ChoiceSystem } from '../systems/ChoiceSystem' type EventHandler = (...args: any[]) => void export class Engine { sceneManager: SceneManager videoManager: VideoManager stateManager: StateManager choiceSystem: ChoiceSystem private currentScene: SceneNode | null = null private events: Map> = new Map() private ended = false private isInitialScene = true constructor() { this.sceneManager = new SceneManager() this.videoManager = new VideoManager() this.stateManager = new StateManager() this.choiceSystem = new ChoiceSystem() } on(event: EngineEvent, handler: EventHandler) { if (!this.events.has(event)) this.events.set(event, new Set()) this.events.get(event)!.add(handler) } off(event: EngineEvent, handler: EventHandler) { this.events.get(event)?.delete(handler) } private emit(event: EngineEvent, ...args: any[]) { this.events.get(event)?.forEach((h) => h(...args)) } start() { this.ended = false this.isInitialScene = true const startScene = this.sceneManager.getStartScene() this.goToScene(startScene) } private goToScene(scene: SceneNode) { this.currentScene = scene if (scene.onEnter) { this.stateManager.apply(scene.onEnter) } const preloadUrls = this.sceneManager.getCandidateUrls( scene, (conds) => conds ? this.stateManager.evaluate(conds) : true ) this.videoManager.onEnd(() => { this.emit('videoEnd', scene) this.onVideoEnd(scene) }) if (this.isInitialScene) { this.isInitialScene = false this.videoManager.playInitial(scene.videoUrl, preloadUrls) } else { this.videoManager.switchTo(scene.videoUrl, preloadUrls) } this.emit('sceneChange', scene) } private onVideoEnd(scene: SceneNode) { const validChoices = this.getValidChoices(scene) if (validChoices.length > 0) { this.emit('choiceRequest', validChoices) this.choiceSystem.start( validChoices, (timerState) => { this.emit('choiceTimer', timerState) }, (defaultChoice) => { this.emit('choiceTimeout', defaultChoice) this.makeChoice(defaultChoice) } ) } else if (scene.nextScene) { const next = this.sceneManager.getScene(scene.nextScene) if (next) { this.goToScene(next) } else { this.endGame() } } else { this.endGame() } } getValidChoices(scene: SceneNode): Choice[] { if (!scene.choices) return [] return scene.choices.filter((c) => !c.conditions || this.stateManager.evaluate(c.conditions) ) } makeChoice(choice: Choice) { if (!this.currentScene) return if (choice.effects) { this.stateManager.apply(choice.effects) } this.stateManager.recordChoice({ sceneId: this.currentScene.id, choiceIndex: this.currentScene.choices?.indexOf(choice) ?? -1, choiceText: choice.text, }) const next = this.sceneManager.getScene(choice.targetScene) if (next) { this.goToScene(next) } else { this.endGame() } } endGame() { this.ended = true this.emit('gameEnd') } resumeScene(sceneId: string, savedState: { variables: Record; flags: string[]; history: any[] }) { this.stateManager.variables = { ...savedState.variables } this.stateManager.flags = new Set(savedState.flags) this.stateManager.history = [...savedState.history] const scene = this.sceneManager.getScene(sceneId) if (!scene) { this.endGame() return } this.ended = false this.isInitialScene = false const preloadUrls = this.sceneManager.getCandidateUrls( scene, (conds) => conds ? this.stateManager.evaluate(conds) : true ) this.videoManager.switchTo(scene.videoUrl, preloadUrls) this.videoManager.onEnd(() => { this.emit('videoEnd', scene) this.onVideoEnd(scene) }) this.currentScene = scene this.emit('sceneChange', scene) } destroy() { this.videoManager.detach() this.events.clear() } }