Files
xsl 866f363591 项目整理:重写主文档、归档过时文档、清理死代码、新增音频制作手册
文档整理:
- 重写 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
2026-06-19 22:29:04 +08:00

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;
}
}