- Reorder onEnd callback before play() in Engine.goToScene to prevent missed ended event if video ends synchronously - Wait for loadedmetadata event in VideoManager.play() before seeking to ensure currentTime reset works correctly on new video sources
104 lines
2.4 KiB
TypeScript
104 lines
2.4 KiB
TypeScript
import type { SceneNode, Choice, EngineEvent } from '../types'
|
|
import { SceneManager } from './SceneManager'
|
|
import { VideoManager } from './VideoManager'
|
|
import { StateManager } from './StateManager'
|
|
|
|
type EventHandler = (...args: any[]) => void
|
|
|
|
export class Engine {
|
|
sceneManager: SceneManager
|
|
videoManager: VideoManager
|
|
stateManager: StateManager
|
|
|
|
private currentScene: SceneNode | null = null
|
|
private events: Map<EngineEvent, Set<EventHandler>> = new Map()
|
|
private ended: boolean = false
|
|
|
|
constructor() {
|
|
this.sceneManager = new SceneManager()
|
|
this.videoManager = new VideoManager()
|
|
this.stateManager = new StateManager()
|
|
}
|
|
|
|
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
|
|
const startScene = this.sceneManager.getStartScene()
|
|
this.goToScene(startScene)
|
|
}
|
|
|
|
private goToScene(scene: SceneNode) {
|
|
this.currentScene = scene
|
|
|
|
if (scene.onEnter) {
|
|
this.stateManager.apply(scene.onEnter)
|
|
}
|
|
|
|
this.videoManager.onEnd(() => {
|
|
this.emit('videoEnd', scene)
|
|
this.onVideoEnd(scene)
|
|
})
|
|
|
|
this.videoManager.play(scene.videoUrl)
|
|
this.emit('sceneChange', scene)
|
|
}
|
|
|
|
private onVideoEnd(scene: SceneNode) {
|
|
if (scene.choices && scene.choices.length > 0) {
|
|
this.emit('choiceRequest', scene.choices)
|
|
} else if (scene.nextScene) {
|
|
const next = this.sceneManager.getScene(scene.nextScene)
|
|
if (next) {
|
|
this.goToScene(next)
|
|
} else {
|
|
this.endGame()
|
|
}
|
|
} else {
|
|
this.endGame()
|
|
}
|
|
}
|
|
|
|
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')
|
|
}
|
|
|
|
destroy() {
|
|
this.videoManager.detach()
|
|
this.events.clear()
|
|
}
|
|
}
|