init
This commit is contained in:
103
engine/core/Engine.ts
Normal file
103
engine/core/Engine.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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.play(scene.videoUrl)
|
||||
this.emit('sceneChange', scene)
|
||||
|
||||
this.videoManager.onEnd(() => {
|
||||
this.emit('videoEnd', scene)
|
||||
this.onVideoEnd(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()
|
||||
}
|
||||
}
|
||||
25
engine/core/SceneManager.ts
Normal file
25
engine/core/SceneManager.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { GameData, SceneNode } from '../types'
|
||||
|
||||
export class SceneManager {
|
||||
private scenes: Record<string, SceneNode> = {}
|
||||
private startScene: string = ''
|
||||
|
||||
load(data: GameData) {
|
||||
this.scenes = data.scenes
|
||||
this.startScene = data.startScene
|
||||
}
|
||||
|
||||
getScene(id: string): SceneNode | undefined {
|
||||
return this.scenes[id]
|
||||
}
|
||||
|
||||
getStartScene(): SceneNode {
|
||||
const scene = this.scenes[this.startScene]
|
||||
if (!scene) throw new Error(`Start scene "${this.startScene}" not found`)
|
||||
return scene
|
||||
}
|
||||
|
||||
getAllSceneIds(): string[] {
|
||||
return Object.keys(this.scenes)
|
||||
}
|
||||
}
|
||||
94
engine/core/StateManager.ts
Normal file
94
engine/core/StateManager.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { Condition, Effect, ChoiceRecord } from '../types'
|
||||
|
||||
export class StateManager {
|
||||
variables: Record<string, number> = {}
|
||||
flags: Set<string> = new Set()
|
||||
history: ChoiceRecord[] = []
|
||||
|
||||
init(initialVars: Record<string, number>) {
|
||||
this.variables = { ...initialVars }
|
||||
this.flags = new Set()
|
||||
this.history = []
|
||||
}
|
||||
|
||||
getVar(name: string): number {
|
||||
return this.variables[name] ?? 0
|
||||
}
|
||||
|
||||
setVar(name: string, value: number) {
|
||||
this.variables[name] = value
|
||||
}
|
||||
|
||||
addVar(name: string, delta: number) {
|
||||
this.variables[name] = (this.variables[name] ?? 0) + delta
|
||||
}
|
||||
|
||||
hasFlag(name: string): boolean {
|
||||
return this.flags.has(name)
|
||||
}
|
||||
|
||||
setFlag(name: string) {
|
||||
this.flags.add(name)
|
||||
}
|
||||
|
||||
clearFlag(name: string) {
|
||||
this.flags.delete(name)
|
||||
}
|
||||
|
||||
evaluate(conditions: Condition[]): boolean {
|
||||
return conditions.every((c) => {
|
||||
switch (c.op) {
|
||||
case '==':
|
||||
return this.variables[c.variable] === c.value
|
||||
case '!=':
|
||||
return this.variables[c.variable] !== c.value
|
||||
case '>':
|
||||
return (this.variables[c.variable] ?? 0) > (c.value as number)
|
||||
case '<':
|
||||
return (this.variables[c.variable] ?? 0) < (c.value as number)
|
||||
case '>=':
|
||||
return (this.variables[c.variable] ?? 0) >= (c.value as number)
|
||||
case '<=':
|
||||
return (this.variables[c.variable] ?? 0) <= (c.value as number)
|
||||
case 'hasFlag':
|
||||
return this.flags.has(c.variable)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
apply(effects: Effect[]) {
|
||||
for (const e of effects) {
|
||||
switch (e.type) {
|
||||
case 'set':
|
||||
this.variables[e.target] = e.value as number
|
||||
break
|
||||
case 'add':
|
||||
this.addVar(e.target, e.value as number)
|
||||
break
|
||||
case 'toggleFlag':
|
||||
this.setFlag(e.target)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recordChoice(choice: ChoiceRecord) {
|
||||
this.history.push(choice)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
variables: { ...this.variables },
|
||||
flags: [...this.flags],
|
||||
history: [...this.history],
|
||||
}
|
||||
}
|
||||
|
||||
fromJSON(data: { variables: Record<string, number>; flags: string[]; history: ChoiceRecord[] }) {
|
||||
this.variables = { ...data.variables }
|
||||
this.flags = new Set(data.flags)
|
||||
this.history = [...data.history]
|
||||
}
|
||||
}
|
||||
58
engine/core/VideoManager.ts
Normal file
58
engine/core/VideoManager.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
type VideoEndCallback = () => void
|
||||
type TimeUpdateCallback = (time: number) => void
|
||||
|
||||
export class VideoManager {
|
||||
private videoEl: HTMLVideoElement | null = null
|
||||
private onEndCallback: VideoEndCallback | null = null
|
||||
private onTimeCallback: TimeUpdateCallback | null = null
|
||||
private lastSrc: string = ''
|
||||
|
||||
attach(videoEl: HTMLVideoElement) {
|
||||
this.videoEl = videoEl
|
||||
videoEl.addEventListener('ended', this.handleEnded)
|
||||
videoEl.addEventListener('timeupdate', this.handleTimeUpdate)
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (!this.videoEl) return
|
||||
this.videoEl.removeEventListener('ended', this.handleEnded)
|
||||
this.videoEl.removeEventListener('timeupdate', this.handleTimeUpdate)
|
||||
this.videoEl = null
|
||||
}
|
||||
|
||||
play(src: string) {
|
||||
if (!this.videoEl) return
|
||||
if (this.lastSrc !== src) {
|
||||
this.videoEl.src = src
|
||||
this.lastSrc = src
|
||||
}
|
||||
this.videoEl.currentTime = 0
|
||||
this.videoEl.play().catch(() => {})
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.videoEl?.pause()
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
return this.videoEl?.currentTime ?? 0
|
||||
}
|
||||
|
||||
onEnd(cb: VideoEndCallback) {
|
||||
this.onEndCallback = cb
|
||||
}
|
||||
|
||||
onTimeUpdate(cb: TimeUpdateCallback) {
|
||||
this.onTimeCallback = cb
|
||||
}
|
||||
|
||||
private handleEnded = () => {
|
||||
this.onEndCallback?.()
|
||||
}
|
||||
|
||||
private handleTimeUpdate = () => {
|
||||
if (this.videoEl) {
|
||||
this.onTimeCallback?.(this.videoEl.currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user