/* 2048.js — Complete Version with WASD + Interactive Effects + Sound Controls */ /* ------------------------ State & Variables ------------------------ */ let board = []; let currentScore = 0; let bestScore = parseInt(localStorage.getItem('bestScore2048')) || 0; let lastMoveDir = null; let isMoving = false; let mergesInCurrentMove = 0; /* ------------------------ Audio Setup ------------------------ */ const audio = { bg: new Audio("Bgmusic.mp3"), pop: new Audio("Pop.mp3"), merge: new Audio("Merge.mp3") }; // Sound State (baca dari localStorage atau default ON) let soundState = { bg: localStorage.getItem('sound_bg') !== 'false', pop: localStorage.getItem('sound_pop') !== 'false', merge: localStorage.getItem('sound_merge') !== 'false' }; // Update audio volumes based on state function updateAudioVolumes() { audio.bg.volume = soundState.bg ? 0.25 : 0; audio.pop.volume = soundState.pop ? 0.9 : 0; audio.merge.volume = soundState.merge ? 1.0 : 0; } audio.bg.loop = true; function tryPlayBg() { if (!soundState.bg) return; // Jangan play kalau muted audio.bg.play().catch(() => { const unlock = () => { if (soundState.bg) audio.bg.play().catch(()=>{}); window.removeEventListener("keydown", unlock); window.removeEventListener("click", unlock); }; window.addEventListener("keydown", unlock, { once: true }); window.addEventListener("click", unlock, { once: true }); }); } /* ------------------------ DOM Ready ------------------------ */ document.addEventListener("DOMContentLoaded", () => { updateBestScoreDisplay(); setupBoard(); addNewTile(); addNewTile(); updateAudioVolumes(); // Apply saved sound settings tryPlayBg(); document.addEventListener("keydown", handleKey); setupEventListeners(); }); /* ------------------------ Event Listeners Setup ------------------------ */ function setupEventListeners() { // Tutorial Modal const btnTutorial = document.getElementById('btn-tutorial'); const tutorialOverlay = document.getElementById('tutorial-overlay'); const closeTutorial = document.getElementById('close-tutorial'); if (btnTutorial) { btnTutorial.addEventListener('click', function() { tutorialOverlay.style.display = 'flex'; }); } if (closeTutorial) { closeTutorial.addEventListener('click', function() { tutorialOverlay.style.display = 'none'; }); } if (tutorialOverlay) { tutorialOverlay.addEventListener('click', function(e) { if (e.target === tutorialOverlay) { tutorialOverlay.style.display = 'none'; } }); } // Restart button (top right) const btnRestart = document.getElementById('btn-restart'); if (btnRestart) { btnRestart.addEventListener('click', restartGame); } // Game over modal buttons const btnPlayAgain = document.getElementById('btn-play-again'); const btnHome = document.getElementById('btn-home'); const gameOverClose = document.getElementById('game-over-close'); if (btnPlayAgain) { btnPlayAgain.addEventListener('click', playAgain); } if (btnHome) { btnHome.addEventListener('click', goHome); } if (gameOverClose) { gameOverClose.addEventListener('click', hideGameOver); } const gameOverOverlay = document.getElementById('game-over-overlay'); if (gameOverOverlay) { gameOverOverlay.addEventListener('click', function(e) { if (e.target === this) { hideGameOver(); } }); } // Sound Control Buttons const btnSoundBg = document.getElementById('btn-sound-bg'); const btnSoundPop = document.getElementById('btn-sound-pop'); const btnSoundMerge = document.getElementById('btn-sound-merge'); if (btnSoundBg) { btnSoundBg.addEventListener('click', () => toggleSound('bg')); updateSoundButtonState(btnSoundBg, soundState.bg); } if (btnSoundPop) { btnSoundPop.addEventListener('click', () => toggleSound('pop')); updateSoundButtonState(btnSoundPop, soundState.pop); } if (btnSoundMerge) { btnSoundMerge.addEventListener('click', () => toggleSound('merge')); updateSoundButtonState(btnSoundMerge, soundState.merge); } } /* ------------------------ Setup & Rendering ------------------------ */ function setupBoard() { board = []; currentScore = 0; updateScoreDisplay(); const container = document.getElementById("board"); if (!container) { console.error("Board element not found (#board)."); return; } container.innerHTML = ""; for (let r = 0; r < 4; r++) { board[r] = []; for (let c = 0; c < 4; c++) { board[r][c] = 0; const tile = document.createElement("div"); tile.id = `${r}-${c}`; tile.className = "tile"; container.appendChild(tile); } } } function updateTile(row, col, num) { const tile = document.getElementById(`${row}-${col}`); if (!tile) return; tile.className = "tile"; if (num > 0) { tile.textContent = num; tile.classList.add("tile-" + num); } else { tile.textContent = ""; } } function refreshBoard() { for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { updateTile(r, c, board[r][c]); } } updateScoreDisplay(); } /* ------------------------ Score Management ------------------------ */ function updateScoreDisplay() { const scoreEl = document.getElementById("score"); if (scoreEl) { scoreEl.textContent = currentScore; } if (currentScore > bestScore) { bestScore = currentScore; localStorage.setItem('bestScore2048', bestScore); updateBestScoreDisplay(); } } function updateBestScoreDisplay() { const bestScoreEl = document.getElementById('best-score'); if (bestScoreEl) { bestScoreEl.textContent = bestScore; } } function resetScore() { currentScore = 0; updateScoreDisplay(); } /* ------------------------ Add New Tile ------------------------ */ function addNewTile() { const empty = []; for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (board[r][c] === 0) empty.push({ r, c }); } } if (empty.length === 0) return false; const spot = empty[Math.floor(Math.random() * empty.length)]; board[spot.r][spot.c] = 2; const tile = document.getElementById(`${spot.r}-${spot.c}`); if (tile) { tile.classList.add("new"); playSound(audio.pop); setTimeout(() => tile.classList.remove("new"), 300); } updateTile(spot.r, spot.c, 2); return true; } /* Safe playSound with mute check */ function playSound(soundObj) { try { // Check if sound is enabled if (soundObj === audio.pop && !soundState.pop) return; if (soundObj === audio.merge && !soundState.merge) return; if (soundObj === audio.bg && !soundState.bg) return; soundObj.currentTime = 0; soundObj.play().catch(() => {}); } catch (e) {} } /* ------------------------ Movement Logic ------------------------ */ function filterZero(row) { return row.filter(n => n !== 0); } function slide(row) { row = filterZero(row); let mergedThisMove = false; let mergedPositions = []; let mergeCount = 0; for (let i = 0; i < row.length - 1; i++) { if (row[i] === row[i + 1]) { row[i] = row[i] * 2; playSound(audio.merge); if (navigator.vibrate) { navigator.vibrate([80, 20, 80]); } currentScore += row[i]; row[i + 1] = 0; mergedThisMove = true; mergedPositions.push(i); mergeCount++; } } row = filterZero(row); while (row.length < 4) row.push(0); return { row, merged: mergedThisMove, mergedPositions, mergeCount }; } function arraysEqual(a, b) { return a.length === b.length && a.every((v, i) => v === b[i]); } /* Move functions */ function moveLeft() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; for (let r = 0; r < 4; r++) { const { row: newRow, mergedPositions, mergeCount } = slide(board[r]); if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; mergesInCurrentMove += mergeCount; if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(c => { mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } function moveRight() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; for (let r = 0; r < 4; r++) { let reversed = [...board[r]].reverse(); const { row: slid, mergedPositions, mergeCount } = slide(reversed); let newRow = slid.reverse(); if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; mergesInCurrentMove += mergeCount; if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(pos => { const c = 3 - pos; mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } function moveUp() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; for (let c = 0; c < 4; c++) { const col = [board[0][c], board[1][c], board[2][c], board[3][c]]; const { row: newCol, mergedPositions, mergeCount } = slide(col); for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } mergesInCurrentMove += mergeCount; if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(r => { mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } function moveDown() { let moved = false; let mergedCells = []; mergesInCurrentMove = 0; for (let c = 0; c < 4; c++) { const col = [board[3][c], board[2][c], board[1][c], board[0][c]]; const { row: slid, mergedPositions, mergeCount } = slide(col); const newCol = slid.reverse(); for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } mergesInCurrentMove += mergeCount; if (mergedPositions && mergedPositions.length > 0) { mergedPositions.forEach(pos => { const r = 3 - pos; mergedCells.push({ r, c }); }); } } if (moved) { refreshBoard(); triggerComboEffect(mergedCells, mergesInCurrentMove); } return moved; } /* ------------------------ Input Handling ------------------------ */ function handleKey(e) { if (isMoving) return; let moved = false; if (e.key === "ArrowLeft") { e.preventDefault(); moved = moveLeft(); } else if (e.key === "ArrowRight") { e.preventDefault(); moved = moveRight(); } else if (e.key === "ArrowUp") { e.preventDefault(); moved = moveUp(); } else if (e.key === "ArrowDown") { e.preventDefault(); moved = moveDown(); } else if (e.key === "a" || e.key === "A") { e.preventDefault(); moved = moveLeft(); } else if (e.key === "d" || e.key === "D") { e.preventDefault(); moved = moveRight(); } else if (e.key === "w" || e.key === "W") { e.preventDefault(); moved = moveUp(); } else if (e.key === "s" || e.key === "S") { e.preventDefault(); moved = moveDown(); } if (moved) { isMoving = true; setTimeout(() => { const added = addNewTile(); if (!added || !canMove()) { setTimeout(() => showGameOver(), 300); } isMoving = false; }, 100); } else { const b = document.getElementById("board"); if (b) { b.classList.add("shake"); setTimeout(()=>b.classList.remove("shake"), 400); } } } function canMove() { for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (board[r][c] === 0) return true; } } for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { const current = board[r][c]; if (c < 3 && board[r][c + 1] === current) return true; if (r < 3 && board[r + 1][c] === current) return true; } } return false; } /* ------------------------ Touch Swipe ------------------------ */ let touchStartX = 0; let touchStartY = 0; document.addEventListener("touchstart", function (e) { const t = e.touches[0]; touchStartX = t.clientX; touchStartY = t.clientY; }, { passive: true }); document.addEventListener("touchend", function (e) { if (isMoving) return; const t = e.changedTouches[0]; const dx = t.clientX - touchStartX; const dy = t.clientY - touchStartY; let moved = false; if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) { if (dx > 0) { moved = moveRight(); } else { moved = moveLeft(); } } else if (Math.abs(dy) > 30) { if (dy > 0) { moved = moveDown(); } else { moved = moveUp(); } } if (moved) { isMoving = true; setTimeout(() => { const added = addNewTile(); if (!added || !canMove()) { setTimeout(() => showGameOver(), 300); } isMoving = false; }, 100); } }, { passive: true }); /* ------------------------ Game Controls ------------------------ */ function restartGame() { hideGameOver(); resetScore(); setupBoard(); addNewTile(); addNewTile(); refreshBoard(); isMoving = false; } function playAgain() { restartGame(); } function goHome() { try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} window.location.href = "Homepage.html"; } /* ------------------------ Game Over Modal ------------------------ */ function showGameOver() { const finalScore = currentScore; const isNewHighScore = finalScore >= bestScore && finalScore > 0; const finalScoreEl = document.getElementById('final-score'); if (finalScoreEl) { finalScoreEl.textContent = finalScore; } const newHighScoreBadge = document.getElementById('new-high-score-badge'); const bestScoreDisplay = document.getElementById('best-score-display'); if (isNewHighScore) { if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block'; if (bestScoreDisplay) bestScoreDisplay.style.display = 'none'; } else { if (newHighScoreBadge) newHighScoreBadge.style.display = 'none'; if (bestScoreDisplay) bestScoreDisplay.style.display = 'block'; const modalBestScore = document.getElementById('modal-best-score'); if (modalBestScore) modalBestScore.textContent = bestScore; } const gameOverOverlay = document.getElementById('game-over-overlay'); if (gameOverOverlay) { gameOverOverlay.style.display = 'flex'; } } function hideGameOver() { const gameOverOverlay = document.getElementById('game-over-overlay'); if (gameOverOverlay) { gameOverOverlay.style.display = 'none'; } } /* ============================================= SOUND CONTROLS ============================================= */ /* ============================================= ADVANCED VOLUME CONTROL SYSTEM ============================================= */ // Volume State (0-100 for each sound) let volumeState = { music: parseInt(localStorage.getItem('vol_music')) || 25, pop: parseInt(localStorage.getItem('vol_pop')) || 90, merge: parseInt(localStorage.getItem('vol_merge')) || 100 }; // Apply volumes on load function initVolumeControl() { // Set audio volumes audio.bg.volume = volumeState.music / 100; audio.pop.volume = volumeState.pop / 100; audio.merge.volume = volumeState.merge / 100; // Update sliders const musicSlider = document.getElementById('vol-music'); const popSlider = document.getElementById('vol-pop'); const mergeSlider = document.getElementById('vol-merge'); if (musicSlider) { musicSlider.value = volumeState.music; updateSliderFill(musicSlider, volumeState.music); document.getElementById('vol-music-display').textContent = volumeState.music + '%'; } if (popSlider) { popSlider.value = volumeState.pop; updateSliderFill(popSlider, volumeState.pop); document.getElementById('vol-pop-display').textContent = volumeState.pop + '%'; } if (mergeSlider) { mergeSlider.value = volumeState.merge; updateSliderFill(mergeSlider, volumeState.merge); document.getElementById('vol-merge-display').textContent = volumeState.merge + '%'; } updateMainSoundIcon(); // Event listeners for sliders if (musicSlider) { musicSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value); volumeState.music = val; audio.bg.volume = val / 100; localStorage.setItem('vol_music', val); document.getElementById('vol-music-display').textContent = val + '%'; updateSliderFill(e.target, val); updateMainSoundIcon(); // Auto-play BG music if volume > 0 if (val > 0 && audio.bg.paused) { tryPlayBg(); } else if (val === 0) { audio.bg.pause(); } }); } if (popSlider) { 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(); }); } if (mergeSlider) { 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(); }); } // Toggle panel visibility const btnSoundMain = document.getElementById('btn-sound-main'); const volumePanel = document.getElementById('volume-panel'); if (btnSoundMain && volumePanel) { btnSoundMain.addEventListener('click', (e) => { e.stopPropagation(); const isVisible = volumePanel.style.display === 'block'; volumePanel.style.display = isVisible ? 'none' : 'block'; }); // Close panel when clicking outside document.addEventListener('click', (e) => { if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target)) { volumePanel.style.display = 'none'; } }); // Prevent panel click from closing volumePanel.addEventListener('click', (e) => { e.stopPropagation(); }); } } // Update slider fill effect function updateSliderFill(slider, value) { slider.style.setProperty('--value', value + '%'); } // Update main sound icon based on volumes function updateMainSoundIcon() { const btnMain = document.getElementById('btn-sound-main'); if (!btnMain) return; const iconFull = btnMain.querySelector('.sound-full'); const iconMedium = btnMain.querySelector('.sound-medium'); const iconLow = btnMain.querySelector('.sound-low'); const iconMuted = btnMain.querySelector('.sound-muted'); // Calculate total volume average const totalVolume = volumeState.music + volumeState.pop + volumeState.merge; const avgVolume = totalVolume / 3; // Hide all icons first iconFull.style.display = 'none'; iconMedium.style.display = 'none'; iconLow.style.display = 'none'; iconMuted.style.display = 'none'; // Show appropriate icon based on average if (totalVolume === 0) { iconMuted.style.display = 'block'; btnMain.classList.add('all-muted'); } else { btnMain.classList.remove('all-muted'); if (avgVolume >= 60) { iconFull.style.display = 'block'; } else if (avgVolume >= 30) { iconMedium.style.display = 'block'; } else { iconLow.style.display = 'block'; } } } // Initialize on DOM load (add this to your existing DOMContentLoaded) document.addEventListener("DOMContentLoaded", () => { // ... existing code ... initVolumeControl(); // ADD THIS LINE }); /* ============================================= COMBO EFFECTS ============================================= */ function triggerComboEffect(mergedCells, comboCount) { if (mergedCells.length === 0) return; mergedCells.forEach(cell => { const tile = document.getElementById(`${cell.r}-${cell.c}`); if (!tile) return; tile.classList.add('merge'); setTimeout(() => tile.classList.remove('merge'), 300); createParticleBurst(tile); tile.style.boxShadow = '0 0 40px currentColor'; setTimeout(() => { tile.style.boxShadow = ''; }, 300); const rect = tile.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const tileValue = parseInt(tile.textContent); createScorePopup(centerX, centerY, tileValue); }); if (comboCount >= 2) { showComboPopup(comboCount); } } function showComboPopup(comboCount) { const board = document.getElementById('board'); if (!board) return; const rect = board.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const popup = document.createElement('div'); popup.className = 'combo-popup'; popup.style.left = centerX + 'px'; popup.style.top = centerY + 'px'; popup.style.position = 'fixed'; popup.style.fontWeight = '900'; popup.style.pointerEvents = 'none'; popup.style.zIndex = '9999'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.textTransform = 'uppercase'; popup.style.letterSpacing = '3px'; if (comboCount === 2) { popup.textContent = 'COMBO x2!'; popup.style.fontSize = '36px'; popup.style.color = '#00ff99'; popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)'; } else if (comboCount === 3) { popup.textContent = 'AMAZING x3!'; popup.style.fontSize = '42px'; popup.style.color = '#ff00ff'; popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)'; } else if (comboCount >= 4) { popup.textContent = 'PERFECT x' + comboCount + '!'; popup.style.fontSize = '48px'; popup.style.color = '#ffd700'; popup.style.textShadow = '0 0 40px rgba(255, 215, 0, 1), 0 0 70px rgba(255, 215, 0, 0.7)'; } document.body.appendChild(popup); popup.animate([ { transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', opacity: 0 }, { transform: 'translate(-50%, -50%) scale(1.3) rotate(5deg)', opacity: 1, offset: 0.3 }, { transform: 'translate(-50%, -50%) scale(1.1) rotate(-2deg)', opacity: 1, offset: 0.6 }, { transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)', opacity: 0 } ], { duration: 1200, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' }).onfinish = () => popup.remove(); } function createParticleBurst(tileElement) { const rect = tileElement.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const tileValue = parseInt(tileElement.textContent); const tileColor = getTileColor(tileValue); const particleCount = 8 + Math.floor(Math.random() * 5); for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); particle.className = 'merge-particle'; particle.style.left = centerX + 'px'; particle.style.top = centerY + 'px'; particle.style.background = tileColor; document.body.appendChild(particle); const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5; const velocity = 60 + Math.random() * 40; const tx = Math.cos(angle) * velocity; const ty = Math.sin(angle) * velocity; particle.animate([ { transform: 'translate(0, 0) scale(1)', opacity: 1 }, { transform: `translate(${tx}px, ${ty}px) scale(0)`, opacity: 0 } ], { duration: 500 + Math.random() * 200, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' }).onfinish = () => particle.remove(); } } function createScorePopup(x, y, score) { const popup = document.createElement('div'); popup.className = 'score-popup'; popup.textContent = '+' + score; popup.style.left = x + 'px'; popup.style.top = y + 'px'; popup.style.position = 'fixed'; popup.style.fontSize = '24px'; popup.style.fontWeight = '900'; popup.style.color = '#ffd700'; popup.style.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)'; popup.style.pointerEvents = 'none'; popup.style.zIndex = '9999'; popup.style.transform = 'translate(-50%, -50%)'; document.body.appendChild(popup); popup.animate([ { transform: 'translate(-50%, -50%) scale(0.5)', opacity: 0 }, { transform: 'translate(-50%, -70px) scale(1.2)', opacity: 1, offset: 0.3 }, { transform: 'translate(-50%, -120px) scale(1)', opacity: 0 } ], { duration: 1000, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' }).onfinish = () => popup.remove(); } function getTileColor(value) { const colors = { 2: '#00eaff', 4: '#00ff99', 8: '#ff00ff', 16: '#ff0066', 32: '#ffaa00', 64: '#ff0000', 128: '#5f00ff', 256: '#00ffea', 512: '#ff00aa', 1024: '#00ffaa', 2048: '#ffd700' }; return colors[value] || '#00eaff'; }