feat(game): 添加游戏大厅音频控制和用户协议功能

- 实现音频资源配置和音频商店状态管理
- 添加用户协议和游戏规则的多语言支持
- 集成音频播放解锁机制和声音开关功能
- 更新API客户端以支持根路径候选
- 优化游戏历史记录组件的滚动加载逻辑
- 添加桌面端控制按钮的动画效果和交互反馈
- 实现语言切换和音效控制的UI组件
- 增加下注相关的状态管理和错误提示
- 完善应用偏好设置的存储和持久化逻辑
This commit is contained in:
JiaJun
2026-05-16 18:02:59 +08:00
parent 5dd4e31db4
commit 85b4d9481f
46 changed files with 1500 additions and 362 deletions

View File

@@ -0,0 +1,19 @@
import hallMusic from '@/assets/music/hall-music.mp3'
export type AudioAssetId = 'hall-bgm'
export type AudioAssetDefinition = {
id: AudioAssetId
loop?: boolean
src: string
volume?: number
}
export const AUDIO_ASSET_DEFINITIONS: AudioAssetDefinition[] = [
{
id: 'hall-bgm',
src: hallMusic,
loop: true,
volume: 1,
},
]

View File

@@ -0,0 +1,103 @@
import { useEffect } from 'react'
import {
AUDIO_ASSET_DEFINITIONS,
type AudioAssetDefinition,
} from '@/features/game/audio/audio-config'
import { useAudioStore } from '@/store'
function createAudioInstance(definition: AudioAssetDefinition) {
const audio = new Audio(definition.src)
audio.preload = 'auto'
audio.loop = definition.loop ?? false
audio.volume = definition.volume ?? 1
return audio
}
export function GlobalAudioController() {
const hasUnlockedSoundPlayback = useAudioStore(
(state) => state.hasUnlockedSoundPlayback,
)
const isSoundEnabled = useAudioStore((state) => state.isSoundEnabled)
useEffect(() => {
const audioEntries = AUDIO_ASSET_DEFINITIONS.map((definition) => ({
audio: createAudioInstance(definition),
definition,
}))
let isDisposed = false
let detachResumeListeners: (() => void) | null = null
const stopAllAudio = () => {
audioEntries.forEach(({ audio }) => {
audio.pause()
audio.currentTime = 0
})
}
const playEnabledAudio = async () => {
const audioState = useAudioStore.getState()
if (
isDisposed ||
!audioState.hasUnlockedSoundPlayback ||
!audioState.isSoundEnabled
) {
return
}
const playResults = await Promise.allSettled(
audioEntries.map(async ({ audio }) => {
audio.currentTime = 0
await audio.play()
}),
)
const hasBlockedAudio = playResults.some(
(result) => result.status === 'rejected',
)
if (!hasBlockedAudio || detachResumeListeners) {
return
}
const resumePlayback = () => {
detachResumeListeners?.()
detachResumeListeners = null
void playEnabledAudio()
}
const events: Array<keyof WindowEventMap> = [
'pointerdown',
'keydown',
'touchstart',
]
events.forEach((eventName) => {
window.addEventListener(eventName, resumePlayback, { once: true })
})
detachResumeListeners = () => {
events.forEach((eventName) => {
window.removeEventListener(eventName, resumePlayback)
})
}
}
if (hasUnlockedSoundPlayback && isSoundEnabled) {
void playEnabledAudio()
} else {
stopAllAudio()
}
return () => {
isDisposed = true
detachResumeListeners?.()
stopAllAudio()
}
}, [hasUnlockedSoundPlayback, isSoundEnabled])
return null
}