feat: audio system, demo scene updates, docs, and engine improvements

This commit is contained in:
2026-06-08 23:18:33 +08:00
parent 514c8f5207
commit 4bfdfbc27d
8 changed files with 316 additions and 24 deletions

View File

@@ -4,6 +4,7 @@ import { VideoManager } from './VideoManager'
import { StateManager } from './StateManager'
import { ChoiceSystem } from '../systems/ChoiceSystem'
import { QTESystem } from '../systems/QTESystem'
import { AudioSystem } from '../systems/AudioSystem'
type EventHandler = (...args: any[]) => void
@@ -13,6 +14,7 @@ export class Engine {
stateManager: StateManager
choiceSystem: ChoiceSystem
qteSystem: QTESystem
audioSystem: AudioSystem
private currentScene: SceneNode | null = null
private events: Map<EngineEvent, Set<EventHandler>> = new Map()
@@ -29,6 +31,7 @@ export class Engine {
this.stateManager = new StateManager()
this.choiceSystem = new ChoiceSystem()
this.qteSystem = new QTESystem()
this.audioSystem = new AudioSystem()
this.videoManager.onTimeUpdate(this.onTimeUpdate)
}
@@ -63,6 +66,25 @@ export class Engine {
this.stateManager.apply(scene.onEnter)
}
if (scene.videoMuted) {
this.videoManager.setMuted(true)
} else {
this.videoManager.setMuted(false)
}
const bgmUrl = scene.bgmUrl || null
if (bgmUrl) {
this.audioSystem.play(
bgmUrl,
scene.bgmVolume ?? 0.8,
scene.bgmCrossFade ?? 2.0,
scene.bgmDuckLevel,
scene.bgmDuckFade,
)
} else {
this.audioSystem.stop(scene.bgmCrossFade ?? 2.0)
}
if (scene.type === 'image') {
this.justCameFromImage = true
this.isInitialScene = false
@@ -123,6 +145,8 @@ export class Engine {
this.emit('qteTrigger', qte)
this.audioSystem.duckOn('qte')
this.qteSystem.trigger(
qte,
(remaining, total) => {
@@ -130,6 +154,7 @@ export class Engine {
},
(success) => {
this.qteResolved = true
this.audioSystem.duckOff('qte')
if (success) {
if (qte.effects?.success) {
this.stateManager.apply(qte.effects.success)
@@ -167,6 +192,7 @@ export class Engine {
const validChoices = this.getValidChoices(scene)
if (validChoices.length > 0) {
this.emit('choiceRequest', validChoices)
this.audioSystem.duckOn('choice')
this.choiceSystem.start(
validChoices,
(timerState) => {
@@ -196,6 +222,12 @@ export class Engine {
})
this.emit('hotspotUpdate', visible)
if (visible.length > 0) {
this.audioSystem.duckOn('hotspot')
} else {
this.audioSystem.duckOff('hotspot')
}
}
getVisibleHotspots(scene: SceneNode): Hotspot[] {
@@ -209,6 +241,8 @@ export class Engine {
clickHotspot(hotspot: Hotspot) {
if (!this.currentScene) return
this.audioSystem.duckOff('hotspot')
if (hotspot.effects) {
this.stateManager.apply(hotspot.effects)
}
@@ -234,6 +268,7 @@ export class Engine {
if (validChoices.length > 0) {
this.emit('choiceRequest', validChoices)
this.audioSystem.duckOn('choice')
this.choiceSystem.start(
validChoices,
@@ -241,6 +276,7 @@ export class Engine {
this.emit('choiceTimer', timerState)
},
(defaultChoice) => {
this.audioSystem.duckOff('choice')
this.emit('choiceTimeout', defaultChoice)
this.makeChoice(defaultChoice)
}
@@ -269,6 +305,8 @@ export class Engine {
makeChoice(choice: Choice) {
if (!this.currentScene) return
this.audioSystem.duckOff('choice')
if (choice.effects) {
this.stateManager.apply(choice.effects)
}
@@ -291,6 +329,7 @@ export class Engine {
this.ended = true
this.loopActive = false
this.qteSystem.cancel()
this.audioSystem.stop(2.0)
this.emit('gameEnd')
}
@@ -338,6 +377,7 @@ export class Engine {
destroy() {
this.qteSystem.destroy()
this.audioSystem.destroy()
this.videoManager.detach()
this.events.clear()
}

View File

@@ -154,6 +154,11 @@ export class VideoManager {
this.active.currentTime = time
}
setMuted(muted: boolean) {
if (this.elA) this.elA.muted = muted
if (this.elB) this.elB.muted = muted
}
onEnd(cb: VideoEndCallback) {
this.onEndCallback = cb
}