// engine.js — 场景引擎 // // 场景数据结构: // narrate : string | fn(state)→string 旁白文本(TTS 兜底) // narrateAudio : string | fn(state)→string 旁白预录 MP3 路径(优先于 TTS) // audio : string[] 角色语音(播放完后才显示选项) // bgm : string | null | undefined undefined=不变 null=停止 string=切换 // delay : number 自动跳转前的等待毫秒 // tap : { label, narrate, narrateAudio, audio, state, next } 单击操作 // doubleTap : { label, narrate, narrateAudio, audio, state, next } 双击操作 // next : string | fn(state) | null 无选项时自动跳转(null=结局) // // state/next 可以是函数 fn(currentState)=>值 class SceneEngine { constructor() { this._game = null; this._state = {}; this._scene = null; this._waiting = false; this._input = null; this._onEnd = null; } // ── 运行游戏 ── async run(game, input, onEnd) { this._game = game; this._state = JSON.parse(JSON.stringify(game.initialState || {})); this._scene = null; this._waiting = false; this._input = input; this._onEnd = onEnd; // 接管输入 input.onTap = () => this._tap(); input.onDoubleTap = () => this._dt(); // 清场 Narrator.stop(); Snd.stopVoice(); Snd.stopBGM(0); Snd.stopAmbience(); dbg(`[engine] start ${game.id}`); if (game.bgm) Snd.startBGM(game.bgm, { vol: 0.18 }); if (game.ambience) Snd.startAmbience(game.ambience, { vol: 0.15 }); await Narrator.sayAndWait(game.intro); await this._goto(game.first); } // ── 跳转到场景 ── async _goto(id) { if (id === null || id === undefined) { this._ending(); return; } const scene = this._game.scenes[id]; if (!scene) { console.error('[engine] missing scene:', id); return; } this._scene = scene; this._waiting = false; Narrator.clearOptions(); dbg(`[scene] ${id}`); // BGM 切换 if (scene.bgm !== undefined) { if (scene.bgm) Snd.startBGM(scene.bgm, { vol: 0.18 }); else Snd.stopBGM(1500); } // 播放音频(等完) const sceneAudio = this._r(scene.audio); if (sceneAudio && sceneAudio.length > 0) { await this._playAudio(sceneAudio); } // 旁白描述(等完)— 优先播放预录音频,否则 TTS if (scene.narrateAudio || scene.narrate) { await this._sayNarrate(scene); } // 有选项 → 等待输入 if (scene.tap || scene.doubleTap) { this._waiting = true; Narrator.options( scene.tap ? scene.tap.label : null, scene.doubleTap ? scene.doubleTap.label : null, ); return; } // 无选项 → 自动跳转 if (scene.delay) await new Promise(r => setTimeout(r, scene.delay)); await this._goto(this._r(scene.next)); } // ── 执行一个操作 ── async _exec(action) { this._waiting = false; Narrator.clearOptions(); Narrator.stop(); // 打断正在播报的选项提示 Haptic.select(); // 更新状态 if (action.state) { const updates = this._r(action.state); Object.assign(this._state, updates); } // 反应音频(支持函数型) const actionAudio = this._r(action.audio); if (actionAudio && actionAudio.length > 0) { await this._playAudio(actionAudio); } // 反应旁白 if (action.narrateAudio || action.narrate) { await this._sayNarrate(action); } await this._goto(this._r(action.next)); } _tap() { if (!this._waiting || !this._scene?.tap) return; this._exec(this._scene.tap); } _dt() { if (!this._waiting || !this._scene?.doubleTap) return; this._exec(this._scene.doubleTap); } // ── 结局 ── async _ending() { Snd.stopBGM(3000); Snd.stopAmbience(); const id = this._game.getEnding(this._state); const e = this._game.endings?.[id]; dbg(`[ending] ${id}`); SFX_UI.ending(); if (e) { await this._sayNarrate(e); } else { await Narrator.sayAndWait('游戏结束。'); } await new Promise(r => setTimeout(r, 2000)); await Narrator.sayAndWait('感谢体验。'); await new Promise(r => setTimeout(r, 3000)); if (this._onEnd) this._onEnd(); } // ── 旁白:优先预录 MP3,兜底 TTS ── _sayNarrate(node) { const audioPath = this._r(node.narrateAudio); if (audioPath) { return this._playAudio([audioPath]); } const text = this._r(node.narrate); if (text) return Narrator.sayAndWait(text); return Promise.resolve(); } // ── 播放多段音频(顺序,等完)── _playAudio(urls) { return new Promise(resolve => { Snd.playVoice(urls, { onEnd: resolve }); }); } // ── 解析 string | fn(state) ── _r(val) { return typeof val === 'function' ? val(this._state) : val; } }