文档整理: - 重写 game/操作说明.md 为项目主文档(6游戏/6手势/旁白/模型生成音频/TTS两步流程) - 归档过时文档至 other_docs/(林夏原始设计、旧执行文档) - 新增 docs/音频制作执行手册.md(从零制作游戏音频的完整流程) - docs/ 下保留旁白音色提示词、音频脚本 代码清理: - 删除旧架构死代码: story.js / gestures.js / profile.js - 删除耦合旧架构的过时测试目录 game/tests/ 脚本配置修正: - check_tts_env.sh: 启动脚本路径 AutoVideo → tts-server(实际位置) - gen_narrator_samples.py: 硬编码地址改为环境变量,默认对齐 tts-server 端口 8000
181 lines
5.0 KiB
JavaScript
181 lines
5.0 KiB
JavaScript
// 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;
|
|
}
|
|
}
|