Files
SoundGame/web/engine/game.js
T
SoundGame Dev cc0f8c008d HTML5 重写:6 款游戏 + 用户系统 + 全局设置 + 5 轮迭代重构
- 新增 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 文档
2026-08-29 21:55:44 +08:00

107 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}