This commit is contained in:
Jevinca Marvella 2025-12-01 21:30:04 +07:00
commit f0a61b13bc
8 changed files with 816 additions and 967 deletions

View File

@ -381,5 +381,11 @@
</div>
<script src="Score_Request.js"></script>
<script src="2048.js"></script>
<script src="Audio_2048.js"></script>
<script src="Visual_Effects_2048.js"></script>
<script src="User_Interface_2048.js"></script>
<script src="2048_Logic.js"></script>
<script src="2048_Controls.js"></script>
<script src="Main_2048.js"></script>
</body>
</html>

972
2048.js
View File

@ -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';
}
};

71
2048_Controls.js Normal file
View File

@ -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 });

195
2048_Logic.js Normal file
View File

@ -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;
}

168
Audio_2048.js Normal file
View File

@ -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';
}
}
}

94
Main_2048.js Normal file
View File

@ -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();
});

126
User_Interface_2048.js Normal file
View File

@ -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';
}

151
Visual_Effects_2048.js Normal file
View File

@ -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';
}