export const DEFAULT_RUNTIME_CLICK_SOUND_SRC = '/audio/ui-click-soft.wav'; export const DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC = '/audio/ui-level-clear.wav'; export const DEFAULT_RUNTIME_MERGE_SOUND_SRC = DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC; export const DEFAULT_RUNTIME_COUNTDOWN_SOUND_SRC = '/audio/ui-countdown-warning.wav'; export const DEFAULT_RUNTIME_COUNTDOWN_WARNING_THRESHOLD_MS = 5_000; export const DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG = { clickSoundSrc: DEFAULT_RUNTIME_CLICK_SOUND_SRC, levelClearSoundSrc: DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC, countdownSoundSrc: DEFAULT_RUNTIME_COUNTDOWN_SOUND_SRC, countdownWarningThresholdMs: DEFAULT_RUNTIME_COUNTDOWN_WARNING_THRESHOLD_MS, } as const; const runtimeAudioCache = new Map(); function clampRuntimeAudioVolume(value: number) { if (!Number.isFinite(value)) { return 0.6; } return Math.max(0, Math.min(1, value)); } export function playRuntimeClickSound( source = DEFAULT_RUNTIME_CLICK_SOUND_SRC, volume = 0.6, ) { if (import.meta.env.MODE === 'test' || typeof Audio === 'undefined') { return; } const normalizedSource = source.trim(); if (!normalizedSource) { return; } const audio = runtimeAudioCache.get(normalizedSource) ?? new Audio(normalizedSource); runtimeAudioCache.set(normalizedSource, audio); audio.currentTime = 0; audio.volume = clampRuntimeAudioVolume(volume); try { const playResult = audio.play(); void playResult?.catch?.(() => { // 中文注释:浏览器可能在用户手势外拒绝播放,点击反馈不应中断主交互。 }); } catch { // 中文注释:测试环境或极端浏览器可能未实现 play,同样不能影响主交互。 } } export function playRuntimeLevelClearSound(volume = 0.6) { playRuntimeClickSound(DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC, volume); } export function playRuntimeMergeSound(volume = 0.6) { playRuntimeClickSound(DEFAULT_RUNTIME_MERGE_SOUND_SRC, volume); } export function playRuntimeCountdownSound(volume = 0.6) { playRuntimeClickSound(DEFAULT_RUNTIME_COUNTDOWN_SOUND_SRC, volume); } export function resolveRuntimeCountdownSecondBucket(remainingMs: number) { if ( !Number.isFinite(remainingMs) || remainingMs <= 0 || remainingMs > DEFAULT_RUNTIME_COUNTDOWN_WARNING_THRESHOLD_MS ) { return null; } return Math.max(1, Math.ceil(remainingMs / 1000)); }