/** * 音效系统 * Phase 3 UI美化 - 音效支持 * * 注意:uni-app的音频功能有限,不同平台支持情况不同 * H5端:使用Web Audio API * 小程序端:使用 wx.createInnerAudioContext() * App端:使用 plus.audio */ /** * 音效类型枚举 */ export const SOUND_TYPES = { // UI音效 CLICK: 'click', OPEN: 'open', CLOSE: 'close', TOGGLE: 'toggle', // 游戏音效 COMBAT_START: 'combat_start', COMBAT_HIT: 'combat_hit', COMBAT_HIT_PLAYER: 'combat_hit_player', COMBAT_VICTORY: 'combat_victory', COMBAT_DEFEAT: 'combat_defeat', // 奖励音效 REWARD: 'reward', LEVEL_UP: 'level_up', SKILL_UP: 'skill_up', // 错误音效 ERROR: 'error', WARNING: 'warning', // 系统音效 NOTIFICATION: 'notification', MESSAGE: 'message' } /** * 音效配置 */ const SOUND_CONFIG = { // 是否启用音效 enabled: true, // 音量 (0-1) volume: 0.5, // 音效文件路径配置 (需要实际文件) soundPaths: { [SOUND_TYPES.CLICK]: '/static/sounds/click.mp3', [SOUND_TYPES.OPEN]: '/static/sounds/open.mp3', [SOUND_TYPES.CLOSE]: '/static/sounds/close.mp3', [SOUND_TYPES.TOGGLE]: '/static/sounds/toggle.mp3', [SOUND_TYPES.COMBAT_START]: '/static/sounds/combat_start.mp3', [SOUND_TYPES.COMBAT_HIT]: '/static/sounds/combat_hit.mp3', [SOUND_TYPES.COMBAT_HIT_PLAYER]: '/static/sounds/combat_hit_player.mp3', [SOUND_TYPES.COMBAT_VICTORY]: '/static/sounds/combat_victory.mp3', [SOUND_TYPES.COMBAT_DEFEAT]: '/static/sounds/combat_defeat.mp3', [SOUND_TYPES.REWARD]: '/static/sounds/reward.mp3', [SOUND_TYPES.LEVEL_UP]: '/static/sounds/level_up.mp3', [SOUND_TYPES.SKILL_UP]: '/static/sounds/skill_up.mp3', [SOUND_TYPES.ERROR]: '/static/sounds/error.mp3', [SOUND_TYPES.WARNING]: '/static/sounds/warning.mp3', [SOUND_TYPES.NOTIFICATION]: '/static/sounds/notification.mp3', [SOUND_TYPES.MESSAGE]: '/static/sounds/message.mp3' } } /** * 音频上下文缓存 */ let audioContext = null let audioCache = {} /** * 初始化音频系统 */ export function initSoundSystem() { // #ifdef H5 try { audioContext = new (window.AudioContext || window.webkitAudioContext)() } catch (e) { console.warn('Web Audio API not supported:', e) } // #endif // #ifdef MP-WEIXIN // 小程序端使用微信音频API // #endif // 从本地存储加载音效设置 loadSoundSettings() } /** * 播放音效 * @param {String} soundType - 音效类型 * @param {Object} options - 选项 { volume, speed } */ export function playSound(soundType, options = {}) { if (!SOUND_CONFIG.enabled) { return { success: false, message: '音效已禁用' } } const volume = options.volume !== undefined ? options.volume : SOUND_CONFIG.volume // #ifdef H5 return playSoundH5(soundType, volume) // #endif // #ifdef MP-WEIXIN return playSoundWeixin(soundType, volume) // #endif // #ifdef APP-PLUS return playSoundApp(soundType, volume) // #endif return { success: false, message: '当前平台不支持音效' } } /** * H5端播放音效 (使用Web Audio API合成简单音效) */ function playSoundH5(soundType, volume) { if (!audioContext) { initSoundSystem() if (!audioContext) { return { success: false, message: '音频上下文初始化失败' } } } // 恢复音频上下文(某些浏览器需要用户交互后才能恢复) if (audioContext.state === 'suspended') { audioContext.resume() } try { // 创建振荡器生成简单音效 const oscillator = audioContext.createOscillator() const gainNode = audioContext.createGain() oscillator.connect(gainNode) gainNode.connect(audioContext.destination) // 根据音效类型设置不同的频率和包络 const soundParams = getSoundParameters(soundType) oscillator.type = soundParams.type || 'sine' oscillator.frequency.setValueAtTime(soundParams.frequency, audioContext.currentTime) // 设置音量包络 const now = audioContext.currentTime gainNode.gain.setValueAtTime(0, now) gainNode.gain.linearRampToValueAtTime(volume * 0.3, now + 0.01) gainNode.gain.exponentialRampToValueAtTime(0.001, now + soundParams.duration) oscillator.start(now) oscillator.stop(now + soundParams.duration) return { success: true } } catch (e) { console.warn('播放音效失败:', e) return { success: false, message: e.message } } } /** * 获取音效参数 (用于合成音效) */ function getSoundParameters(soundType) { const params = { // 点击音效 - 短促的高音 [SOUND_TYPES.CLICK]: { type: 'sine', frequency: 800, duration: 0.05 }, // 打开音效 - 上升音调 [SOUND_TYPES.OPEN]: { type: 'sine', frequency: 400, duration: 0.15 }, // 关闭音效 - 下降音调 [SOUND_TYPES.CLOSE]: { type: 'sine', frequency: 300, duration: 0.1 }, // 切换音效 [SOUND_TYPES.TOGGLE]: { type: 'square', frequency: 500, duration: 0.08 }, // 战斗开始 - 低沉 [SOUND_TYPES.COMBAT_START]: { type: 'sawtooth', frequency: 150, duration: 0.3 }, // 命中敌人 - 尖锐 [SOUND_TYPES.COMBAT_HIT]: { type: 'square', frequency: 1200, duration: 0.08 }, // 被命中 - 低频 [SOUND_TYPES.COMBAT_HIT_PLAYER]: { type: 'sawtooth', frequency: 200, duration: 0.15 }, // 胜利 - 上升音阶 [SOUND_TYPES.COMBAT_VICTORY]: { type: 'sine', frequency: 523, duration: 0.4 }, // 失败 - 下降音 [SOUND_TYPES.COMBAT_DEFEAT]: { type: 'sawtooth', frequency: 100, duration: 0.4 }, // 奖励 - 悦耳的高音 [SOUND_TYPES.REWARD]: { type: 'sine', frequency: 880, duration: 0.2 }, // 升级 - 双音 [SOUND_TYPES.LEVEL_UP]: { type: 'sine', frequency: 659, duration: 0.3 }, // 技能升级 [SOUND_TYPES.SKILL_UP]: { type: 'sine', frequency: 784, duration: 0.25 }, // 错误 - 低频嗡鸣 [SOUND_TYPES.ERROR]: { type: 'sawtooth', frequency: 100, duration: 0.2 }, // 警告 [SOUND_TYPES.WARNING]: { type: 'square', frequency: 440, duration: 0.15 }, // 通知 [SOUND_TYPES.NOTIFICATION]: { type: 'sine', frequency: 660, duration: 0.2 }, // 消息 [SOUND_TYPES.MESSAGE]: { type: 'sine', frequency: 587, duration: 0.15 } } return params[soundType] || { type: 'sine', frequency: 440, duration: 0.1 } } /** * 微信小程序端播放音效 */ function playSoundWeixin(soundType, volume) { // #ifdef MP-WEIXIN const soundPath = SOUND_CONFIG.soundPaths[soundType] if (!soundPath) { return { success: false, message: '音效文件未配置' } } try { const audio = uni.createInnerAudioContext() audio.src = soundPath audio.volume = volume audio.play() return { success: true } } catch (e) { return { success: false, message: e.message } } // #endif return { success: false, message: '非微信小程序环境' } } /** * App端播放音效 */ function playSoundApp(soundType, volume) { // #ifdef APP-PLUS const soundPath = SOUND_CONFIG.soundPaths[soundType] if (!soundPath) { return { success: false, message: '音效文件未配置' } } try { const player = plus.audio.createPlayer(soundPath) player.setVolume(volume * 100) player.play() return { success: true } } catch (e) { return { success: false, message: e.message } } // #endif return { success: false, message: '非App环境' } } /** * 设置音效开关 * @param {Boolean} enabled - 是否启用音效 */ export function setSoundEnabled(enabled) { SOUND_CONFIG.enabled = enabled saveSoundSettings() } /** * 设置音量 * @param {Number} volume - 音量 (0-1) */ export function setSoundVolume(volume) { SOUND_CONFIG.volume = Math.max(0, Math.min(1, volume)) saveSoundSettings() } /** * 获取音效设置 * @returns {Object} { enabled, volume } */ export function getSoundSettings() { return { enabled: SOUND_CONFIG.enabled, volume: SOUND_CONFIG.volume } } /** * 保存音效设置到本地存储 */ function saveSoundSettings() { try { uni.setStorageSync('soundSettings', JSON.stringify({ enabled: SOUND_CONFIG.enabled, volume: SOUND_CONFIG.volume })) } catch (e) { console.warn('保存音效设置失败:', e) } } /** * 从本地存储加载音效设置 */ function loadSoundSettings() { try { const settings = uni.getStorageSync('soundSettings') if (settings) { const parsed = JSON.parse(settings) SOUND_CONFIG.enabled = parsed.enabled !== undefined ? parsed.enabled : true SOUND_CONFIG.volume = parsed.volume !== undefined ? parsed.volume : 0.5 } } catch (e) { console.warn('加载音效设置失败:', e) } } /** * 快捷音效播放函数 */ export const playClick = () => playSound(SOUND_TYPES.CLICK) export const playOpen = () => playSound(SOUND_TYPES.OPEN) export const playClose = () => playSound(SOUND_TYPES.CLOSE) export const playCombatStart = () => playSound(SOUND_TYPES.COMBAT_START) export const playCombatHit = () => playSound(SOUND_TYPES.COMBAT_HIT) export const playCombatVictory = () => playSound(SOUND_TYPES.COMBAT_VICTORY) export const playCombatDefeat = () => playSound(SOUND_TYPES.COMBAT_DEFEAT) export const playReward = () => playSound(SOUND_TYPES.REWARD) export const playLevelUp = () => playSound(SOUND_TYPES.LEVEL_UP) export const playSkillUp = () => playSound(SOUND_TYPES.SKILL_UP) export const playError = () => playSound(SOUND_TYPES.ERROR) export const playWarning = () => playSound(SOUND_TYPES.WARNING) // 初始化音效系统 initSoundSystem()