feat: engine improvements, new scenes, videos, subtitles, hotspot component and docs update

This commit is contained in:
2026-06-08 14:01:58 +08:00
parent e68ed9c962
commit 6b67989007
20 changed files with 354 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
import type { SceneNode, Choice, EngineEvent } from '../types'
import type { SceneNode, Choice, EngineEvent, Hotspot } from '../types'
import { SceneManager } from './SceneManager'
import { VideoManager } from './VideoManager'
import { StateManager } from './StateManager'
@@ -60,6 +60,16 @@ export class Engine {
this.stateManager.apply(scene.onEnter)
}
if (scene.type === 'image') {
this.isInitialScene = false
this.emit('sceneChange', scene)
const visible = this.getVisibleHotspots(scene)
if (visible.length > 0) {
this.emit('hotspotRequest', visible)
}
return
}
const preloadUrls = this.sceneManager.getCandidateUrls(
scene,
(conds) => conds ? this.stateManager.evaluate(conds) : true
@@ -84,7 +94,11 @@ export class Engine {
private checkQTE = (time: number) => {
const scene = this.currentScene
if (!scene?.qte || this.qteTriggered) return
if (!scene) return
this.checkHotspotTime(scene, time)
if (!scene.qte || this.qteTriggered) return
if (time >= scene.qte.triggerTime) {
this.qteTriggered = true
const qte = scene.qte
@@ -126,6 +140,48 @@ export class Engine {
}
}
private checkHotspotTime(scene: SceneNode, time: number) {
if (!scene.hotspots || scene.hotspots.length === 0) return
const visible = scene.hotspots.filter((hs) => {
if (hs.conditions && !this.stateManager.evaluate(hs.conditions)) return false
if (hs.showAt !== undefined && time < hs.showAt) return false
if (hs.hideAt !== undefined && time >= hs.hideAt) return false
return true
})
this.emit('hotspotUpdate', visible)
}
getVisibleHotspots(scene: SceneNode): Hotspot[] {
if (!scene.hotspots) return []
return scene.hotspots.filter((hs) => {
if (hs.conditions && !this.stateManager.evaluate(hs.conditions)) return false
return true
})
}
clickHotspot(hotspot: Hotspot) {
if (!this.currentScene) return
if (hotspot.effects) {
this.stateManager.apply(hotspot.effects)
}
this.stateManager.recordChoice({
sceneId: this.currentScene.id,
choiceIndex: -1,
choiceText: hotspot.label,
})
const next = this.sceneManager.getScene(hotspot.targetScene)
if (next) {
this.goToScene(next)
} else {
this.endGame()
}
}
private onVideoEnd(scene: SceneNode) {
const validChoices = this.getValidChoices(scene)
@@ -202,6 +258,15 @@ export class Engine {
this.ended = false
this.isInitialScene = false
if (scene.type === 'image') {
this.emit('sceneChange', scene)
const visible = this.getVisibleHotspots(scene)
if (visible.length > 0) {
this.emit('hotspotRequest', visible)
}
return
}
const preloadUrls = this.sceneManager.getCandidateUrls(
scene,
(conds) => conds ? this.stateManager.evaluate(conds) : true