335 lines
9.4 KiB
JavaScript
335 lines
9.4 KiB
JavaScript
|
|
/**
|
|||
|
|
* 音效系统
|
|||
|
|
* 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()
|