352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
/**
|
||
* 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;
|
||
};
|
||
|
||
})();
|