/* ========================================== 2048 AUDIO - SOUND MANAGEMENT SYSTEM ========================================== fitur: 1. 3 Audio objects (bg, pop, merge) 2. Volume control terpisah untuk setiap audio 3. Icon dinamis sesuai level volume 4. Panel volume dengan inputLock integration ========================================== */ /* ========================================== AUDIO OBJECTS ========================================== */ const audio = { bg: new Audio("Background_Music.mp3"), // Background music pop: new Audio("Pop.mp3"), // Sound saat tile spawn merge: new Audio("Merge.mp3") // Sound saat tile merge }; audio.bg.loop = true; // Background music loop terus /* ========================================== UPDATE VOLUME DARI STATE & SLIDERS ========================================== */ function updateAudioVolumes() { // Formula: volume = enabled ? (slider / 100) : 0 audio.bg.volume = soundState.bg ? (volumeState.music / 100) : 0; audio.pop.volume = soundState.pop ? (volumeState.pop / 100) : 0; audio.merge.volume = soundState.merge ? (volumeState.merge / 100) : 0; } /* ========================================== PLAY BACKGROUND MUSIC (dengan unlock) ========================================== */ function tryPlayBg() { if (!soundState.bg || volumeState.music === 0) return; // Coba play, kalau di-block browser (autoplay policy) audio.bg.play().catch(() => { // Setup unlock: tunggu user interaction const unlock = () => { if (soundState.bg && volumeState.music > 0) audio.bg.play().catch(()=>{}); window.removeEventListener("keydown", unlock); window.removeEventListener("click", unlock); }; window.addEventListener("keydown", unlock, { once: true }); window.addEventListener("click", unlock, { once: true }); }); } /* ========================================== PLAY SOUND (dengan mute check) ========================================== */ function playSound(soundObj) { try { // Guard: cek mute state sebelum play if (soundObj === audio.pop && (!soundState.pop || volumeState.pop === 0)) return; if (soundObj === audio.merge && (!soundState.merge || volumeState.merge === 0)) return; if (soundObj === audio.bg && (!soundState.bg || volumeState.music === 0)) return; soundObj.currentTime = 0; // Restart dari awal soundObj.play().catch(() => {}); // Play with error handling } catch (e) {} } /* ========================================== INIT VOLUME SLIDERS - Setup Event Listeners ========================================== */ function initVolumeControl() { updateAudioVolumes(); const musicSlider = document.getElementById('vol-music'); const popSlider = document.getElementById('vol-pop'); const mergeSlider = document.getElementById('vol-merge'); /* --- MUSIC SLIDER --- */ if (musicSlider) { // Load nilai dari localStorage musicSlider.value = volumeState.music; updateSliderFill(musicSlider, volumeState.music); document.getElementById('vol-music-display').textContent = volumeState.music + '%'; // Event listener saat slider digeser musicSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value); volumeState.music = val; // Update state audio.bg.volume = val / 100; // Set volume langsung localStorage.setItem('vol_music', val); // Save ke browser document.getElementById('vol-music-display').textContent = val + '%'; updateSliderFill(e.target, val); // Update visual fill updateMainSoundIcon(); // Update icon // Auto play/pause background music if (val > 0 && audio.bg.paused && soundState.bg) { tryPlayBg(); // Play kalau volume > 0 } else if (val === 0) { audio.bg.pause(); // Pause kalau volume = 0 (muted) } }); } /* --- POP SLIDER (sama seperti music) --- */ if (popSlider) { popSlider.value = volumeState.pop; updateSliderFill(popSlider, volumeState.pop); document.getElementById('vol-pop-display').textContent = volumeState.pop + '%'; popSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value); volumeState.pop = val; audio.pop.volume = val / 100; localStorage.setItem('vol_pop', val); document.getElementById('vol-pop-display').textContent = val + '%'; updateSliderFill(e.target, val); updateMainSoundIcon(); }); } /* --- MERGE SLIDER (sama seperti music) --- */ if (mergeSlider) { mergeSlider.value = volumeState.merge; updateSliderFill(mergeSlider, volumeState.merge); document.getElementById('vol-merge-display').textContent = volumeState.merge + '%'; mergeSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value); volumeState.merge = val; audio.merge.volume = val / 100; localStorage.setItem('vol_merge', val); document.getElementById('vol-merge-display').textContent = val + '%'; updateSliderFill(e.target, val); updateMainSoundIcon(); }); } updateMainSoundIcon(); setupVolumePanelEvents(); // Setup panel interactions } /* ========================================== SETUP PANEL EVENTS - Open/Close Logic ========================================== Ini yang nge-link dengan inputLocked di Controls ========================================== */ function setupVolumePanelEvents() { const btnSoundMain = document.getElementById('btn-sound-main'); const volumePanel = document.getElementById('volume-panel'); const volumeBackdrop = document.getElementById('volume-backdrop'); if (btnSoundMain && volumePanel) { // Event: Klik tombol sound main btnSoundMain.addEventListener('click', (e) => { e.stopPropagation(); // Jangan trigger event di parent const isActive = volumePanel.classList.contains('active'); if (isActive) { // TUTUP PANEL volumePanel.classList.remove('active'); if (volumeBackdrop) volumeBackdrop.classList.remove('active'); inputLocked = false; // UNLOCK - game input aktif lagi } else { // BUKA PANEL volumePanel.classList.add('active'); if (volumeBackdrop) volumeBackdrop.classList.add('active'); inputLocked = true; // LOCK - blokir swipe & keyboard } }); // Event: Klik backdrop (tutup panel) volumeBackdrop.addEventListener('click', () => { volumePanel.classList.remove('active'); volumeBackdrop.classList.remove('active'); inputLocked = false; // UNLOCK }); // Event: Klik di luar panel (desktop) document.addEventListener('click', (e) => { if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target) && (!volumeBackdrop || !volumeBackdrop.contains(e.target))) { volumePanel.classList.remove('active'); if (volumeBackdrop) volumeBackdrop.classList.remove('active'); inputLocked = false; // UNLOCK } }); // Event: Klik di dalam panel (jangan tutup) volumePanel.addEventListener('click', (e) => { e.stopPropagation(); // Prevent close }); } } /* ========================================== UPDATE VISUAL FILL SLIDER ========================================== */ function updateSliderFill(slider, value) { // Set CSS custom property untuk animasi fill // Dipakai di CSS: background: linear-gradient(...) slider.style.setProperty('--value', value + '%'); } /* ========================================== UPDATE ICON DINAMIS - Sesuai Volume Level ========================================== */ function updateMainSoundIcon() { const btnMain = document.getElementById('btn-sound-main'); if (!btnMain) return; // Ambil semua icon const iconFull = btnMain.querySelector('.sound-full'); const iconMedium = btnMain.querySelector('.sound-medium'); const iconLow = btnMain.querySelector('.sound-low'); const iconMuted = btnMain.querySelector('.sound-muted'); // Hitung rata-rata volume dari 3 slider const totalVolume = volumeState.music + volumeState.pop + volumeState.merge; const avgVolume = totalVolume / 3; // Hide semua icon dulu if (iconFull) iconFull.style.display = 'none'; if (iconMedium) iconMedium.style.display = 'none'; if (iconLow) iconLow.style.display = 'none'; if (iconMuted) iconMuted.style.display = 'none'; // Show icon yang sesuai dengan level volume if (totalVolume === 0) { // Semua muted if (iconMuted) iconMuted.style.display = 'block'; btnMain.classList.add('all-muted'); } else { btnMain.classList.remove('all-muted'); if (avgVolume >= 60) { // Volume tinggi (≥60%) if (iconFull) iconFull.style.display = 'block'; } else if (avgVolume >= 30) { // Volume sedang (30-59%) if (iconMedium) iconMedium.style.display = 'block'; } else { // Volume rendah (1-29%) if (iconLow) iconLow.style.display = 'block'; } } }