diff --git a/2048.html b/2048.html index 1609b19..ebecb5f 100644 --- a/2048.html +++ b/2048.html @@ -379,5 +379,11 @@ + + + + + + diff --git a/2048.js b/2048.js index 4d85301..b296052 100644 --- a/2048.js +++ b/2048.js @@ -1,31 +1,21 @@ /* ------------------------ - State & Variables + 1. GAME STATE & VARIABLES ------------------------ */ let board = []; let currentScore = 0; -// 1. Ambil username dari sessionStorage (sesuai sistem login kamu) +// Ambil username dari sessionStorage const currentUser = sessionStorage.getItem("loggedInUser") || "guest"; -// 2. Buat nama kunci unik, misal: "highScore2048_budi" +// Buat nama kunci unik const storageKey = 'highScore2048_' + currentUser; -// 3. Ambil skor milik user tersebut saja - +// Ambil skor milik user tersebut let highScore = parseInt(localStorage.getItem(storageKey)) || 0; let lastMoveDir = null; let isMoving = false; let mergesInCurrentMove = 0; -/* ------------------------ - Audio Setup - ------------------------ */ -const audio = { - bg: new Audio("Background_Music.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', @@ -33,961 +23,9 @@ let soundState = { 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 }); - }); -} - -/* ===== AUTO TUTORIAL FOR FIRST TIME USERS ===== */ -function checkAndShowTutorial() { - const showTutorial = sessionStorage.getItem("showTutorial"); - const loggedInUser = sessionStorage.getItem("loggedInUser"); - - // Jika user baru (showTutorial = "true"), tampilkan tutorial otomatis - if (showTutorial === "true" && loggedInUser) { - setTimeout(() => { - const tutorialOverlay = document.getElementById('tutorial-overlay'); - if (tutorialOverlay) { - tutorialOverlay.style.display = 'flex'; - } - // Set flag agar tidak muncul lagi di session ini - sessionStorage.setItem("showTutorial", "false"); - }, 500); // Delay 500ms agar halaman sudah fully loaded - } -} - -/* ------------------------ - DOM Ready - ------------------------ */ -document.addEventListener("DOMContentLoaded", () => { - updateHighScoreDisplay(); - setupBoard(); - addNewTile(); - addNewTile(); - updateAudioVolumes(); // Apply saved sound settings - tryPlayBg(); - document.addEventListener("keydown", handleKey); - setupEventListeners(); - checkAndShowTutorial(); -}); - -/* ------------------------ - 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 > highScore) { - highScore = currentScore; - // Gunakan storageKey yang sudah kita buat di atas (dinamis sesuai user) - localStorage.setItem(storageKey, highScore); - updateHighScoreDisplay(); -} - } - -function updateHighScoreDisplay() { - const highScoreEl = document.getElementById('high-score'); - if (highScoreEl) { - highScoreEl.textContent = highScore; - } -} - -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; - - // --- TAMBAHKAN BAGIAN INI (MULAI) --- - // Mengecek apakah fungsi saveScore ada (dari file Score_Request.js) - if (typeof saveScore === 'function') { - console.log("Mengirim skor ke database:", finalScore); // Debugging - saveScore(finalScore); - } else { - console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload."); - } - - const isNewHighScore = finalScore >= highScore && finalScore > 0; - - const finalScoreEl = document.getElementById('final-score'); - if (finalScoreEl) { - finalScoreEl.textContent = finalScore; - } - - const newHighScoreBadge = document.getElementById('new-high-score-badge'); - const highScoreDisplay = document.getElementById('high-score-display'); - - if (isNewHighScore) { - if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block'; - if (highScoreDisplay) highScoreDisplay.style.display = 'none'; - } else { - if (newHighScoreBadge) newHighScoreBadge.style.display = 'none'; - if (highScoreDisplay) highScoreDisplay.style.display = 'block'; - const modalHighScore = document.getElementById('modal-high-score'); - if (modalHighScore) modalHighScore.textContent = highScore; - } - - 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'; - } -} - -/* ============================================= - 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'; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/2048_Controls.js b/2048_Controls.js new file mode 100644 index 0000000..2a11374 --- /dev/null +++ b/2048_Controls.js @@ -0,0 +1,71 @@ +/* ------------------------ + 6. INPUT HANDLER + ------------------------ */ +function handleKey(e) { + if (isMoving) return; + + let moved = false; + const k = e.key; + + if (k === "ArrowLeft" || k === "a" || k === "A") { e.preventDefault(); moved = moveLeft(); } + else if (k === "ArrowRight" || k === "d" || k === "D") { e.preventDefault(); moved = moveRight(); } + else if (k === "ArrowUp" || k === "w" || k === "W") { e.preventDefault(); moved = moveUp(); } + else if (k === "ArrowDown" || k === "s" || k === "S") { e.preventDefault(); moved = moveDown(); } + + if (moved) { + isMoving = true; + setTimeout(() => { + const added = addNewTile(); + if (!added || !canMove()) { + setTimeout(() => showGameOver(), 300); + } + isMoving = false; + }, 100); + } else { + // Shake effect on invalid move + const b = document.getElementById("board"); + if (b) { + b.classList.add("shake"); + setTimeout(()=>b.classList.remove("shake"), 400); + } + } +} + +/* 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 }); \ No newline at end of file diff --git a/2048_Logic.js b/2048_Logic.js new file mode 100644 index 0000000..2d7bf53 --- /dev/null +++ b/2048_Logic.js @@ -0,0 +1,195 @@ +/* ------------------------ + 5. GAME LOGIC + ------------------------ */ +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; +} + +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; +} + +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; +} \ No newline at end of file diff --git a/Audio_2048.js b/Audio_2048.js new file mode 100644 index 0000000..f564076 --- /dev/null +++ b/Audio_2048.js @@ -0,0 +1,168 @@ +/* ------------------------ + 2. AUDIO MANAGER + ------------------------ */ +const audio = { + bg: new Audio("Background_Music.mp3"), + pop: new Audio("Pop.mp3"), + merge: new Audio("Merge.mp3") +}; + +audio.bg.loop = true; + +// Update audio volumes based on state & sliders +function updateAudioVolumes() { + 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; +} + +function tryPlayBg() { + if (!soundState.bg || volumeState.music === 0) return; + + audio.bg.play().catch(() => { + 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 }); + }); +} + +/* Safe playSound with mute check */ +function playSound(soundObj) { + try { + 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; + soundObj.play().catch(() => {}); + } catch (e) {} +} + +// Initialize Volume Sliders +function initVolumeControl() { + updateAudioVolumes(); + + 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 + '%'; + + musicSlider.addEventListener('input', (e) => { + const val = parseInt(e.target.value); + volumeState.music = val; + audio.bg.volume = val / 100; // Direct update + localStorage.setItem('vol_music', val); + document.getElementById('vol-music-display').textContent = val + '%'; + updateSliderFill(e.target, val); + updateMainSoundIcon(); + + if (val > 0 && audio.bg.paused && soundState.bg) { + tryPlayBg(); + } else if (val === 0) { + audio.bg.pause(); + } + }); + } + + 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(); + }); + } + + 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(); +} + +function setupVolumePanelEvents() { + 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'; + }); + + document.addEventListener('click', (e) => { + if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target)) { + volumePanel.style.display = 'none'; + } + }); + + volumePanel.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } +} + +function updateSliderFill(slider, value) { + slider.style.setProperty('--value', value + '%'); +} + +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'); + + const totalVolume = volumeState.music + volumeState.pop + volumeState.merge; + const avgVolume = totalVolume / 3; + + if (iconFull) iconFull.style.display = 'none'; + if (iconMedium) iconMedium.style.display = 'none'; + if (iconLow) iconLow.style.display = 'none'; + if (iconMuted) iconMuted.style.display = 'none'; + + if (totalVolume === 0) { + if (iconMuted) iconMuted.style.display = 'block'; + btnMain.classList.add('all-muted'); + } else { + btnMain.classList.remove('all-muted'); + if (avgVolume >= 60) { + if (iconFull) iconFull.style.display = 'block'; + } else if (avgVolume >= 30) { + if (iconMedium) iconMedium.style.display = 'block'; + } else { + if (iconLow) iconLow.style.display = 'block'; + } + } +} \ No newline at end of file diff --git a/Main_2048.js b/Main_2048.js new file mode 100644 index 0000000..3e05a30 --- /dev/null +++ b/Main_2048.js @@ -0,0 +1,94 @@ +/* ------------------------ + 7. MAIN INITIALIZATION + ------------------------ */ +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"; +} + +/* 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', () => tutorialOverlay.style.display = 'flex'); + if (closeTutorial) closeTutorial.addEventListener('click', () => tutorialOverlay.style.display = 'none'); + if (tutorialOverlay) tutorialOverlay.addEventListener('click', (e) => { + if (e.target === tutorialOverlay) tutorialOverlay.style.display = 'none'; + }); + + // Restart & Game Over buttons + const btnRestart = document.getElementById('btn-restart'); + if (btnRestart) btnRestart.addEventListener('click', restartGame); + + const btnPlayAgain = document.getElementById('btn-play-again'); + if (btnPlayAgain) btnPlayAgain.addEventListener('click', playAgain); + + const btnHome = document.getElementById('btn-home'); + if (btnHome) btnHome.addEventListener('click', goHome); + + const gameOverClose = document.getElementById('game-over-close'); + 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 Buttons (Mute Toggles) + 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', () => { + soundState.bg = !soundState.bg; + localStorage.setItem('sound_bg', soundState.bg); + updateAudioVolumes(); + if(soundState.bg) tryPlayBg(); else audio.bg.pause(); + // Tambahkan logika update tombol UI jika ada (toggle class) + }); + + if (btnSoundPop) btnSoundPop.addEventListener('click', () => { + soundState.pop = !soundState.pop; + localStorage.setItem('sound_pop', soundState.pop); + updateAudioVolumes(); + }); + + if (btnSoundMerge) btnSoundMerge.addEventListener('click', () => { + soundState.merge = !soundState.merge; + localStorage.setItem('sound_merge', soundState.merge); + updateAudioVolumes(); + }); +} + +/* DOM Ready */ +document.addEventListener("DOMContentLoaded", () => { + updateHighScoreDisplay(); + setupBoard(); + addNewTile(); + addNewTile(); + initVolumeControl(); // Starts audio logic + tryPlayBg(); + document.addEventListener("keydown", handleKey); + setupEventListeners(); + checkAndShowTutorial(); +}); \ No newline at end of file diff --git a/User_Interface_2048.js b/User_Interface_2048.js new file mode 100644 index 0000000..201c941 --- /dev/null +++ b/User_Interface_2048.js @@ -0,0 +1,126 @@ +/* ------------------------ + 4. UI MANAGER + ------------------------ */ +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(); +} + +function updateScoreDisplay() { + const scoreEl = document.getElementById("score"); + if (scoreEl) { + scoreEl.textContent = currentScore; + } + + if (currentScore > highScore) { + highScore = currentScore; + localStorage.setItem(storageKey, highScore); + updateHighScoreDisplay(); + } +} + +function updateHighScoreDisplay() { + const highScoreEl = document.getElementById('high-score'); + if (highScoreEl) { + highScoreEl.textContent = highScore; + } +} + +function resetScore() { + currentScore = 0; + updateScoreDisplay(); +} + +function checkAndShowTutorial() { + const showTutorial = sessionStorage.getItem("showTutorial"); + const loggedInUser = sessionStorage.getItem("loggedInUser"); + + if (showTutorial === "true" && loggedInUser) { + setTimeout(() => { + const tutorialOverlay = document.getElementById('tutorial-overlay'); + if (tutorialOverlay) { + tutorialOverlay.style.display = 'flex'; + } + sessionStorage.setItem("showTutorial", "false"); + }, 500); + } +} + +function showGameOver() { + const finalScore = currentScore; + + if (typeof saveScore === 'function') { + console.log("Mengirim skor ke database:", finalScore); + saveScore(finalScore); + } else { + console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload."); + } + + const isNewHighScore = finalScore >= highScore && finalScore > 0; + + const finalScoreEl = document.getElementById('final-score'); + if (finalScoreEl) finalScoreEl.textContent = finalScore; + + const newHighScoreBadge = document.getElementById('new-high-score-badge'); + const highScoreDisplay = document.getElementById('high-score-display'); + + if (isNewHighScore) { + if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block'; + if (highScoreDisplay) highScoreDisplay.style.display = 'none'; + } else { + if (newHighScoreBadge) newHighScoreBadge.style.display = 'none'; + if (highScoreDisplay) highScoreDisplay.style.display = 'block'; + const modalHighScore = document.getElementById('modal-high-score'); + if (modalHighScore) modalHighScore.textContent = highScore; + } + + 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'; +} \ No newline at end of file diff --git a/Visual_Effects_2048.js b/Visual_Effects_2048.js new file mode 100644 index 0000000..e09c380 --- /dev/null +++ b/Visual_Effects_2048.js @@ -0,0 +1,151 @@ +/* ------------------------ + 3. VISUAL 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'; +} \ No newline at end of file