/* ------------------------ 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") }; audio.bg.volume = 0.25; audio.pop.volume = 0.9; audio.merge.volume = 1.0; audio.bg.loop = true; function tryPlayBg() { audio.bg.play().catch(() => { const unlock = () => { 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(); 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); } // Close button (X) di game over modal if (gameOverClose) { gameOverClose.addEventListener('click', hideGameOver); } // Close game over when clicking outside modal const gameOverOverlay = document.getElementById('game-over-overlay'); if (gameOverOverlay) { gameOverOverlay.addEventListener('click', function(e) { if (e.target === this) { hideGameOver(); } }); } } /* ------------------------ 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); } } } /* Update single tile visual */ 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 = ""; } } /* Refresh whole board */ 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 - FIXED: Only play pop sound here ------------------------ */ 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"); // ✅ POP SOUND: Hanya main di sini (tile baru muncul) playSound(audio.pop); setTimeout(() => tile.classList.remove("new"), 300); } updateTile(spot.r, spot.c, 2); return true; } /* Safe playSound */ function playSound(soundObj) { try { soundObj.currentTime = 0; soundObj.play().catch(() => {}); } catch (e) {} } /* ------------------------ Movement Logic - FIXED: Merge sound plays consistently ------------------------ */ 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; // ✅ MERGE SOUND & VIBRATION: Selalu main saat merge 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 with COMBO DETECTION */ 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 - WITH WASD SUPPORT ------------------------ */ function handleKey(e) { if (isMoving) return; let moved = false; // Arrow Keys 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(); } // WASD Keys 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); } } } /* Check if any move is possible */ 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'; } } /* ============================================= COMBO EFFECT HANDLER ============================================= */ function triggerComboEffect(mergedCells, comboCount) { if (mergedCells.length === 0) return; // Trigger individual tile effects 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); // Individual score popup 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); }); // Show COMBO popup based on merge count if (comboCount >= 2) { showComboPopup(comboCount); } } /* ============================================= COMBO POPUP ============================================= */ 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'; // Different text and color based on combo count 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); // Animate combo 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(); } /* ============================================= PARTICLE & SCORE EFFECTS ============================================= */ 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'; }