/** * game.js — 游戏侧契约:Game 基类 + StateMachineGame 状态机基类。 * 对齐 Android 的 Game 接口 / GameEngine(见 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),线性剧情类游戏的推荐起点。 * * 子类实现: * 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); } }