/** * AudioManager.js - 音频管理器(框架层) * * 功能: * 1. 统一管理游戏音效和背景音乐的播放/停止 * 2. 封装底层音频API (Utl.playSound/stopSound/playMusic/stopMusic) * 3. 提供根据性别选择语音的通用基础设施 * * ⚠️ 框架设计原则: * - 本文件不包含任何具体游戏的资源常量(不引用 SoundResources) * - 不包含任何游戏专属的 ID 映射(如麻将牌值、动作类型) * - 游戏专属逻辑在子游戏侧封装(如 GameAudio.js) * - 静音开关与音量由平台层(Utl / 平台设置)统一处理,本文件不维护其状态 * * 核心 API: * - AudioManager.playSound(soundFile) // 播放任意音效文件 * - AudioManager.playVoice(baseId, sex) // 播放语音(自动切换男/女音) * - AudioManager.playVoiceBySeat(seat, baseId) // 播放语音(从座位自动获取性别) * - AudioManager.playMusic() // 播放背景音乐 * * 性别判断: * - sex === 2 为女性,女音文件 ID = 男音 baseId + FEMALE_VOICE_OFFSET (100) * - 可通过 Utl.getSexBySeat(seat) 获取座位上玩家的性别 * * 注意: * - 背景音乐需要在 Game_Config.soundList 中配置 * - 游戏专属的 ID 映射(牌值/动作类型等)由子游戏侧 GameAudio.js 维护 * - 是否静音 / 音量大小由平台层 Utl.playSound / Utl.playMusic 内部按用户设置决定 * * ============================================================================ * @guide 开发指南 - AudioManager 使用规范 * ============================================================================ * * 【初始化(UIBootstrap 或游戏入口调用一次)】 * AudioManager.init(); * * 【播放任意音效文件(游戏层传文件名)】 * AudioManager.playSound('00101.mp3'); * // 通常配合游戏侧常量使用: * AudioManager.playSound(SoundResources.EFFECTS.DISCARD); * * 【通用性别语音播放(框架层 API,游戏层控制 ID)】 * * gameabc 引擎语音文件命名规则: * - 文件名统一为 5位数字 + ".mp3",如 "00151.mp3" * - 男音使用基础 ID,女音 = 基础 ID + FEMALE_VOICE_OFFSET(默认 100) * - 例:男音 baseId=151 → "00151.mp3",女音 → "00251.mp3" * * // 指定 baseId 和性别(ID 含义由游戏层定义): * AudioManager.playVoice(151, sex); // 自动切换男/女音 * AudioManager.playVoiceBySeat(seat, 151); // 从座位自动获取性别再播放 * * 【游戏侧扩展模式(在子游戏中创建 GameAudio.js)】 * * // 子游戏自行维护 ID 映射,调用框架的 playVoice/playVoiceBySeat * var ACTION_VOICE_BASE_ID = { * 'peng': 152, 'gang': 153, 'hu': 154, 'chi': 151 * }; * GameAudio.playActionBySeat = function(seat, action) { * var baseId = ACTION_VOICE_BASE_ID[action]; * if (baseId) AudioManager.playVoiceBySeat(seat, baseId); * }; * // 调用:GameAudio.playActionBySeat(seat, 'peng'); * * 【背景音乐】 * AudioManager.playMusic(); // 播放(需在 Game_Config.soundList 中配置) * AudioManager.stopMusic(); * * @version 4.0.0 * @date 2026-06-08 */ var AudioManager = AudioManager || {}; (function() { 'use strict'; // ============================================================================ // 私有变量 // ============================================================================ var _initialized = false; var _currentMusic = null; var _playingSounds = {}; // { soundFile: playCount } // ============================================================================ // 框架常量 - gameabc 引擎语音规则 // ============================================================================ /** * 女性语音 ID 偏移量 * gameabc 引擎约定:女音文件 ID = 男音基础 ID + FEMALE_VOICE_OFFSET */ var FEMALE_VOICE_OFFSET = 100; /** * 女性性别值(来自 Utl.getSexBySeat 返回值约定) */ var SEX_FEMALE = 2; // ============================================================================ // 私有方法 // ============================================================================ /** * 检查 Utl API 是否可用 * @private */ function _isUtlAvailable() { return typeof Utl !== 'undefined'; } /** * 格式化语音文件名 * gameabc 引擎语音文件统一为 5位数字 + .mp3 * @private * @param {number} fileNum * @returns {string} 如 "00151.mp3" */ function _formatSoundFile(fileNum) { var s = String(fileNum); while (s.length < 5) { s = '0' + s; } return s + '.mp3'; } function _isFemale(sex) { return sex === SEX_FEMALE; } /** * 通过座位号获取玩家性别 * @private * @param {number} seat * @returns {number} 性别值(2=女,其他=男) */ function _getSexBySeat(seat) { if (_isUtlAvailable() && typeof Utl.getSexBySeat === 'function') { return Utl.getSexBySeat(seat); } return 1; } /** * 根据性别获取语音 ID 偏移量 * @private * @param {number} sex * @returns {number} 0(男)或 FEMALE_VOICE_OFFSET(女) */ function _getVoiceOffset(sex) { return _isFemale(sex) ? FEMALE_VOICE_OFFSET : 0; } // ============================================================================ // 公共 API - 初始化 // ============================================================================ /** * 初始化音频管理器 * 静音/音量由平台层处理,本管理器无需配置项。 */ AudioManager.init = function() { if (_initialized) { console.warn('[AudioManager] Already initialized'); return; } _initialized = true; // console.log('[AudioManager] Initialized (v4.0.0)'); }; // ============================================================================ // 公共 API - 音效播放 // ============================================================================ /** * 播放音效 * @param {string} soundFile - 文件名,如 "00101.mp3" 或游戏侧常量 * @returns {boolean} */ AudioManager.playSound = function(soundFile) { if (!soundFile) { return false; } if (!_isUtlAvailable() || !Utl.playSound) { console.warn('[AudioManager] Utl.playSound not available'); return false; } try { Utl.playSound(soundFile); _playingSounds[soundFile] = (_playingSounds[soundFile] || 0) + 1; return true; } catch (e) { console.error('[AudioManager] playSound error:', soundFile, e); return false; } }; /** * 停止音效 * @param {string} soundFile * @returns {boolean} */ AudioManager.stopSound = function(soundFile) { if (!soundFile) { return false; } if (!_isUtlAvailable() || !Utl.stopSound) { console.warn('[AudioManager] Utl.stopSound not available'); return false; } try { Utl.stopSound(soundFile); if (_playingSounds[soundFile]) { _playingSounds[soundFile]--; if (_playingSounds[soundFile] <= 0) { delete _playingSounds[soundFile]; } } return true; } catch (e) { console.error('[AudioManager] stopSound error:', soundFile, e); return false; } }; /** 停止所有音效 */ AudioManager.stopAllSounds = function() { for (var f in _playingSounds) { if (_playingSounds.hasOwnProperty(f)) { this.stopSound(f); } } _playingSounds = {}; }; // ============================================================================ // 公共 API - 性别语音(通用) // ============================================================================ /** * 根据 baseId 和性别播放语音 * 框架自动应用偏移:女音 = baseId + FEMALE_VOICE_OFFSET * 游戏层负责维护 baseId 含义(哪个 ID 对应哪张牌/动作) * * @param {number} baseId - 男音基础 ID(如 151) * @param {number} [sex=1] - 性别(2=女,其他=男) * @returns {boolean} */ AudioManager.playVoice = function(baseId, sex) { if (!baseId) { return false; } return this.playSound(_formatSoundFile(baseId + _getVoiceOffset(sex))); }; /** * 从座位自动获取性别后播放语音(推荐用法) * @param {number} seat - 座位号 * @param {number} baseId - 男音基础 ID * @returns {boolean} */ AudioManager.playVoiceBySeat = function(seat, baseId) { return this.playVoice(baseId, _getSexBySeat(seat)); }; /** * 获取语音文件名(不播放,用于预加载或调试) * @param {number} baseId * @param {number} [sex=1] * @returns {string} 如 "00151.mp3" */ AudioManager.getVoiceFile = function(baseId, sex) { return _formatSoundFile(baseId + _getVoiceOffset(sex)); }; // ============================================================================ // 公共 API - 背景音乐 // ============================================================================ /** * 播放背景音乐 * 注意:需在 Game_Config.soundList 中配置 * @returns {boolean} */ AudioManager.playMusic = function() { if (!_isUtlAvailable() || !Utl.playMusic) { console.warn('[AudioManager] Utl.playMusic not available'); return false; } try { Utl.playMusic(); _currentMusic = true; return true; } catch (e) { console.error('[AudioManager] playMusic error:', e); return false; } }; /** * 停止背景音乐 * @returns {boolean} */ AudioManager.stopMusic = function() { if (!_isUtlAvailable() || !Utl.stopMusic) { return false; } try { Utl.stopMusic(); _currentMusic = null; return true; } catch (e) { console.error('[AudioManager] stopMusic error:', e); return false; } }; /** 暂停背景音乐(等同 stopMusic) */ AudioManager.pauseMusic = function() { return this.stopMusic(); }; /** 恢复背景音乐 */ AudioManager.resumeMusic = function() { if (_currentMusic) { return this.playMusic(); } return false; }; // ============================================================================ // 公共API - 状态 // ============================================================================ /** * 获取音频管理器状态 * @returns {Object} */ AudioManager.getStatus = function() { var playingCount = 0; for (var key in _playingSounds) { if (_playingSounds.hasOwnProperty(key)) { playingCount += _playingSounds[key]; } } return { initialized: _initialized, playingSoundsCount: playingCount, isMusicPlaying: !!_currentMusic, version: '4.0.0' }; }; /** * 销毁音频管理器 */ AudioManager.destroy = function() { this.stopAllSounds(); this.stopMusic(); _playingSounds = {}; _currentMusic = null; _initialized = false; }; })();