- 新增 web/ 纯音频 HTML5 平台:共享引擎(手势/WebAudio 三层/设置/震动)、 Node 零依赖服务器(静态 + 注册登录 + 进度存档 API) - 游戏:剑与声(RPG 新作)、声命线、暗室(重设计)、音序回响(重设计)、 节拍行者(跟随领奏重构)、盲航(同向闪避重构);声东击西删除 - 全局功能:男声/女声双套 TTS 资产、三档语速、BGM 音量三档、 震动反馈语言(10 种震型)、服务器进度同步 - 资产:899 ogg + 899 m4a(Qwen3-TTS 双性别语音、Stable Audio 3 音效、 Mureka BGM 4+3 首),终检零缺失 - 文档:docs/design/ 13 篇设计审查文档、docs/audio/ 资产管线、 web/server/README.md API 文档
107 lines
3.4 KiB
JavaScript
107 lines
3.4 KiB
JavaScript
/**
|
||
* game.js — 游戏侧契约:Game 基类 + StateMachineGame 状态机基类。
|
||
* 对齐 Android 的 Game 接口 / GameEngine<S>(见 docs/architecture.md)。
|
||
*
|
||
* 每个游戏的接线方式(见 web/template/index.html 完整示例):
|
||
* const audio = new WebAudioEngine('audio/');
|
||
* const game = new MyGame(audio, document.getElementById('game-root'));
|
||
* await audio.showStartOverlay({ cover: 'cover.png' }); // 解锁音频
|
||
* game.start();
|
||
* attachGestures(document.getElementById('game-root'), (g) => game.onGesture(g));
|
||
*/
|
||
|
||
/**
|
||
* Game 基类(对齐 Android Game 接口)。
|
||
* 直接继承它,覆写三个方法即可;适合状态复杂的游戏
|
||
* (Android 端 storyline/reflex/escape 实际都是直接实现 Game 的)。
|
||
*/
|
||
export class Game {
|
||
/**
|
||
* @param {WebAudioEngine} audio 音频引擎(已解锁后才会调用 start)
|
||
* @param {HTMLElement} root 全屏根元素(#game-root),可用于改封面/DOM 提示
|
||
*/
|
||
constructor(audio, root) {
|
||
this.audio = audio;
|
||
this.root = root;
|
||
}
|
||
|
||
/**
|
||
* 手势入口。g ∈ 'up'|'down'|'left'|'right'|'tap'|'doubletap'
|
||
* 注意 'tap' 回调有约 300ms 延迟(双击判定窗口),即时反馈请用滑动/双击。
|
||
* @param {string} g
|
||
*/
|
||
onGesture(g) { /* 子类实现 */ }
|
||
|
||
/** 音频解锁后由页面调用一次。游戏从这里开始(播 BGM、开场白等)。 */
|
||
start() { /* 子类实现 */ }
|
||
|
||
/** 页面隐藏/退出时调用,应停止计时器与音频。可选实现。 */
|
||
stop() {
|
||
this.audio.stopAll();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 状态机基类(对齐 Android GameEngine<S>),线性剧情类游戏的推荐起点。
|
||
*
|
||
* 子类实现:
|
||
* initialState() -> 初始状态
|
||
* transition(state, g) -> 纯函数,返回新状态;返回原状态表示无转移
|
||
* onEnterState(state, prev) -> 副作用(播音频等),prev 初始为 null
|
||
*
|
||
* onGesture 是 final 逻辑:算 transition,状态变了才更新并回调 onEnterState。
|
||
*
|
||
* 示例:
|
||
* class MyGame extends StateMachineGame {
|
||
* initialState() { return 'INTRO'; }
|
||
* transition(s, g) {
|
||
* if (s === 'INTRO' && g === 'tap') return 'PLAY';
|
||
* return s;
|
||
* }
|
||
* onEnterState(s) {
|
||
* if (s === 'INTRO') this.audio.play('voice/intro', { onComplete: () => ... });
|
||
* }
|
||
* }
|
||
*/
|
||
export class StateMachineGame extends Game {
|
||
constructor(audio, root) {
|
||
super(audio, root);
|
||
this.state = null;
|
||
}
|
||
|
||
/** @returns {*} 初始状态 */
|
||
initialState() { throw new Error('implement initialState()'); }
|
||
|
||
/**
|
||
* 纯函数:由 (当前状态, 手势) 算新状态。返回原状态表示无转移。
|
||
* @param {*} state @param {string} g @returns {*}
|
||
*/
|
||
transition(state, g) { throw new Error('implement transition()'); }
|
||
|
||
/** 进入新状态的副作用(播音频)。prev 初始为 null。 */
|
||
onEnterState(state, prev) { /* 子类实现 */ }
|
||
|
||
onGesture(g) {
|
||
const next = this.transition(this.state, g);
|
||
if (next !== this.state) {
|
||
const prev = this.state;
|
||
this.state = next;
|
||
this.onEnterState(next, prev);
|
||
}
|
||
}
|
||
|
||
/** 强制切换状态(如计时器驱动、语音 onComplete 驱动)。 */
|
||
setState(next) {
|
||
if (next !== this.state) {
|
||
const prev = this.state;
|
||
this.state = next;
|
||
this.onEnterState(next, prev);
|
||
}
|
||
}
|
||
|
||
start() {
|
||
this.state = this.initialState();
|
||
this.onEnterState(this.state, null);
|
||
}
|
||
}
|