feat: video loop support for hotspot scenes, demo updates, docs, and engine fixes

This commit is contained in:
2026-06-08 21:48:47 +08:00
parent 5b40781d0a
commit 0dbe1b097d
6 changed files with 102 additions and 22 deletions

View File

@@ -21,6 +21,7 @@ export class Engine {
private qteTriggered = false
private qteResolved = false
private justCameFromImage = false
private loopActive = false
constructor() {
this.sceneManager = new SceneManager()
@@ -29,7 +30,7 @@ export class Engine {
this.choiceSystem = new ChoiceSystem()
this.qteSystem = new QTESystem()
this.videoManager.onTimeUpdate(this.checkQTE)
this.videoManager.onTimeUpdate(this.onTimeUpdate)
}
on(event: EngineEvent, handler: EventHandler) {
@@ -56,6 +57,7 @@ export class Engine {
this.currentScene = scene
this.qteTriggered = false
this.qteResolved = false
this.loopActive = false
if (scene.onEnter) {
this.stateManager.apply(scene.onEnter)
@@ -106,12 +108,14 @@ export class Engine {
}
}
private checkQTE = (time: number) => {
private onTimeUpdate = (time: number) => {
const scene = this.currentScene
if (!scene) return
this.checkHotspotTime(scene, time)
this.checkLoop(time)
// QTE check after loop check, so loop doesn't interfere with QTE
if (!scene.qte || this.qteTriggered) return
if (time >= scene.qte.triggerTime) {
this.qteTriggered = true
@@ -154,6 +158,33 @@ export class Engine {
}
}
private checkLoop(time: number) {
const scene = this.currentScene
if (!scene?.loopStart) return
if (!this.loopActive && time >= scene.loopStart) {
this.loopActive = true
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)
}
)
}
}
if (this.loopActive && scene.loopEnd && time >= scene.loopEnd) {
this.videoManager.seekTo(scene.loopStart)
}
}
private checkHotspotTime(scene: SceneNode, time: number) {
if (!scene.hotspots || scene.hotspots.length === 0) return
@@ -197,6 +228,8 @@ export class Engine {
}
private onVideoEnd(scene: SceneNode) {
if (this.loopActive) return
const validChoices = this.getValidChoices(scene)
if (validChoices.length > 0) {
@@ -220,7 +253,6 @@ export class Engine {
this.endGame()
}
} else if (scene.hotspots?.length) {
// hotspot-only scene: wait for user to click a hotspot
return
} else {
this.endGame()
@@ -257,6 +289,7 @@ export class Engine {
endGame() {
this.ended = true
this.loopActive = false
this.qteSystem.cancel()
this.emit('gameEnd')
}

View File

@@ -149,6 +149,11 @@ export class VideoManager {
return this.active ?? null
}
seekTo(time: number) {
if (!this.active) return
this.active.currentTime = time
}
onEnd(cb: VideoEndCallback) {
this.onEndCallback = cb
}

View File

@@ -9,6 +9,8 @@ export interface SceneNode {
qte?: QTEDefinition
nextScene?: string
onEnter?: Effect[]
loopStart?: number
loopEnd?: number
}
export interface Choice {