初始化仓库:友乐/gameabc 房卡游戏平台脚手架(client + server + docs)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:13:26 +08:00
co-authored by Claude Opus 5
commit 594820d393
655 changed files with 310861 additions and 0 deletions
@@ -0,0 +1,351 @@
/**
* 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;
};
})();