2048 update

This commit is contained in:
Jevinca Marvella 2025-11-29 20:39:08 +07:00
parent 07be893964
commit e2ba499ce6
3 changed files with 1638 additions and 483 deletions

1205
2048.css

File diff suppressed because it is too large Load Diff

188
2048.html
View File

@ -1,26 +1,172 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048</title> <title>2048</title>
<link rel="stylesheet" href="2048.css"> <link rel="stylesheet" href="2048.css">
</head> <link rel="preconnect" href="https://fonts.googleapis.com">
<body> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- ADD: ambient particle / neon background layer --> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800&display=swap" rel="stylesheet">
<div class="particles" aria-hidden="true"></div> </head>
<div class="starfield"></div> <body>
<div class="cursor-light"></div> <!-- Background Effects -->
<h1>2048</h1> <div class="particles" aria-hidden="true"></div>
<div id="top-menu"> <div class="starfield" aria-hidden="true"></div>
<button class="btn" onclick="restartGame()">Restart</button> <div class="cursor-light" aria-hidden="true"></div>
<button class="btn" onclick="goHome()">Home</button>
<!-- Top Right Controls -->
<div class="top-controls">
<button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
</button>
<button class="icon-btn btn-restart-icon" id="btn-restart" title="Restart Game">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
</svg>
</button>
</div>
<!-- Game Container -->
<div class="game-container">
<!-- Header: Title + Scores -->
<div class="game-header">
<h1>2048</h1>
<div class="score-container">
<div class="score-box">
<div class="score-label">SCORE</div>
<div class="score-value" id="score">0</div>
</div>
<div class="score-box">
<div class="score-label">HIGH SCORE</div>
<div class="score-value" id="best-score">0</div>
</div>
</div>
</div> </div>
<hr> <!-- Game Board -->
<h2>Score: <span id="score">0</span></h2> <div id="board"></div>
<div id="board"> </div>
<!-- Tutorial Modal -->
<div class="tutorial-overlay" id="tutorial-overlay" style="display: none;">
<div class="tutorial-modal">
<button class="modal-close" id="close-tutorial">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<h2 class="tutorial-title">How to Play</h2>
<div class="tutorial-content">
<!-- PC Controls - Hidden on Mobile -->
<div class="tutorial-section pc-controls">
<h3>🖥️ PC Controls</h3>
<div class="keys-container">
<!-- WASD Keys -->
<div class="keys-group">
<div class="keys-grid-wasd">
<div class="key-empty"></div>
<div class="key-box">W</div>
<div class="key-empty"></div>
<div class="key-box">A</div>
<div class="key-box">S</div>
<div class="key-box">D</div>
</div>
<p class="keys-label">WASD</p>
</div>
<div class="keys-separator">or</div>
<!-- Arrow Keys -->
<div class="keys-group">
<div class="keys-grid-arrow">
<div class="key-empty"></div>
<div class="key-box"></div>
<div class="key-empty"></div>
<div class="key-box"></div>
<div class="key-box"></div>
<div class="key-box"></div>
</div>
<p class="keys-label">Arrow Keys</p>
</div>
</div>
</div>
<!-- Mobile Controls - Hidden on PC -->
<div class="tutorial-section mobile-controls">
<h3>📱 Mobile Controls</h3>
<div class="swipe-demo">
<div class="swipe-icon">👆</div>
<p>Swipe in any direction</p>
</div>
</div>
<div class="tutorial-section">
<h3>🎯 Objective</h3>
<p class="objective-text">Combine tiles with the same numbers to reach <strong>2048</strong>!</p>
</div>
</div>
</div> </div>
<script src="2048.js"></script> </div>
</body>
<!-- Game Over Modal -->
<div class="game-over-overlay" id="game-over-overlay" style="display: none;">
<div class="game-over-modal">
<!-- Close Button (X) -->
<button class="game-over-close" id="game-over-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<!-- Title -->
<div class="game-over-title">No More Moves!</div>
<div class="game-over-subtitle">Game Over</div>
<!-- Score Section -->
<div class="game-over-score">
<div class="game-over-score-label">Your Score</div>
<div class="game-over-score-value" id="final-score">0</div>
<!-- New High Score Badge - show if new record -->
<div class="new-high-score" id="new-high-score-badge" style="display: none;">
New High Score
</div>
<!-- Best Score Display - show if NOT new record -->
<div class="best-score-display" id="best-score-display" style="display: none;">
<div class="best-score-label">High Score</div>
<div class="best-score-value" id="modal-best-score">0</div>
</div>
</div>
<!-- Action Buttons - ICON ONLY -->
<div class="game-over-buttons">
<!-- Restart Button -->
<button class="btn-game-icon btn-restart-game" id="btn-play-again" title="Restart Game">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
</svg>
</button>
<!-- Home Button -->
<button class="btn-game-icon btn-home-game" id="btn-home" title="Back to Home">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</button>
</div>
</div>
</div>
<script src="2048.js"></script>
</body>
</html> </html>

728
2048.js
View File

@ -1,15 +1,17 @@
/* 2048.js Enhanced with animations, particles, and glows /* 2048.js — Complete Version with WASD + Interactive Merge Effects */
Replace previous 2048.js content with this file.
*/
/* ------------------------ /* ------------------------
State & audio (kept) State & Variables
------------------------ */ ------------------------ */
let board = []; let board = [];
let score = 0; let currentScore = 0;
let lastMoveDir = null; // 'left','right','up','down' or null let bestScore = parseInt(localStorage.getItem('bestScore2048')) || 0;
let lastMoveDir = null;
let isMoving = false;
// --- Audio setup --- /* ------------------------
Audio Setup
------------------------ */
const audio = { const audio = {
bg: new Audio("bgmusic.mp3"), bg: new Audio("bgmusic.mp3"),
pop: new Audio("pop.mp3"), pop: new Audio("pop.mp3"),
@ -33,24 +35,90 @@ function tryPlayBg() {
} }
/* ------------------------ /* ------------------------
DOM ready DOM Ready
------------------------ */ ------------------------ */
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
updateBestScoreDisplay();
setupBoard(); setupBoard();
addNewTile(); addNewTile();
addNewTile(); addNewTile();
tryPlayBg(); tryPlayBg();
document.addEventListener("keydown", handleKey); document.addEventListener("keydown", handleKey);
setupAmbientCursor(); setupAmbientCursor();
setupEventListeners();
}); });
/* ------------------------ /* ------------------------
Setup & rendering Event Listeners Setup
------------------------ */
function setupEventListeners() {
// Tutorial Modal
const btnTutorial = document.getElementById('btn-tutorial');
const tutorialOverlay = document.getElementById('tutorial-overlay');
const closeTutorial = document.getElementById('close-tutorial');
if (btnTutorial) {
btnTutorial.addEventListener('click', function() {
tutorialOverlay.style.display = 'flex';
});
}
if (closeTutorial) {
closeTutorial.addEventListener('click', function() {
tutorialOverlay.style.display = 'none';
});
}
if (tutorialOverlay) {
tutorialOverlay.addEventListener('click', function(e) {
if (e.target === tutorialOverlay) {
tutorialOverlay.style.display = 'none';
}
});
}
// Restart button (top right)
const btnRestart = document.getElementById('btn-restart');
if (btnRestart) {
btnRestart.addEventListener('click', restartGame);
}
// Game over modal buttons
const btnPlayAgain = document.getElementById('btn-play-again');
const btnHome = document.getElementById('btn-home');
const gameOverClose = document.getElementById('game-over-close');
if (btnPlayAgain) {
btnPlayAgain.addEventListener('click', playAgain);
}
if (btnHome) {
btnHome.addEventListener('click', goHome);
}
// Close button (X) di game over modal
if (gameOverClose) {
gameOverClose.addEventListener('click', hideGameOver);
}
// Close game over when clicking outside modal
const gameOverOverlay = document.getElementById('game-over-overlay');
if (gameOverOverlay) {
gameOverOverlay.addEventListener('click', function(e) {
if (e.target === this) {
hideGameOver();
}
});
}
}
/* ------------------------
Setup & Rendering
------------------------ */ ------------------------ */
function setupBoard() { function setupBoard() {
board = []; board = [];
score = 0; currentScore = 0;
updateScore(); updateScoreDisplay();
const container = document.getElementById("board"); const container = document.getElementById("board");
if (!container) { if (!container) {
@ -71,65 +139,62 @@ function setupBoard() {
} }
} }
/* update single tile visual with small entrance based on last move */ /* Update single tile visual */
function updateTile(row, col, num) { function updateTile(row, col, num) {
const tile = document.getElementById(`${row}-${col}`); const tile = document.getElementById(`${row}-${col}`);
if (!tile) return; if (!tile) return;
// reset classes except base .tile
tile.className = "tile"; tile.className = "tile";
// ensure previous transforms cleared
tile.style.transform = "";
tile.style.opacity = "";
if (num > 0) { if (num > 0) {
tile.textContent = num; tile.textContent = num;
tile.classList.add("tile-" + num); tile.classList.add("tile-" + num);
// slide-illusion: appear from direction of last move
if (lastMoveDir) {
let tx = 0, ty = 0;
const gap = 22; // small px offset for feel
if (lastMoveDir === "left") tx = gap;
else if (lastMoveDir === "right") tx = -gap;
else if (lastMoveDir === "up") ty = gap;
else if (lastMoveDir === "down") ty = -gap;
// start slightly offset & transparent, then animate to 0
tile.style.transform = `translate(${tx}px, ${ty}px)`;
tile.style.opacity = "0.0";
// force reflow then animate back
void tile.offsetWidth;
tile.style.transition = "transform 0.14s cubic-bezier(.2,.8,.2,1), opacity 0.12s";
tile.style.transform = "";
tile.style.opacity = "1";
// cleanup transition after done
setTimeout(() => { tile.style.transition = ""; }, 160);
}
} else { } else {
tile.textContent = ""; tile.textContent = "";
} }
} }
/* refresh whole board */ /* Refresh whole board */
function refreshBoard() { function refreshBoard() {
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
updateTile(r, c, board[r][c]); updateTile(r, c, board[r][c]);
} }
} }
updateScore(); updateScoreDisplay();
} }
/* score */ /* ------------------------
function updateScore() { Score Management
const el = document.getElementById("score"); ------------------------ */
if (el) el.textContent = score; function updateScoreDisplay() {
const scoreEl = document.getElementById("score");
if (scoreEl) {
scoreEl.textContent = currentScore;
}
if (currentScore > bestScore) {
bestScore = currentScore;
localStorage.setItem('bestScore2048', bestScore);
updateBestScoreDisplay();
}
} }
/* add new tile with pop animation */ function updateBestScoreDisplay() {
const bestScoreEl = document.getElementById('best-score');
if (bestScoreEl) {
bestScoreEl.textContent = bestScore;
}
}
function resetScore() {
currentScore = 0;
updateScoreDisplay();
}
/* ------------------------
Add New Tile
------------------------ */
function addNewTile() { function addNewTile() {
const empty = []; const empty = [];
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
@ -147,18 +212,13 @@ function addNewTile() {
if (tile) { if (tile) {
tile.classList.add("new"); tile.classList.add("new");
playSound(audio.pop); playSound(audio.pop);
tile.addEventListener("animationend", function handler() { setTimeout(() => tile.classList.remove("new"), 300);
tile.classList.remove("new");
tile.removeEventListener("animationend", handler);
});
updateTile(spot.r, spot.c, 2);
} else {
updateTile(spot.r, spot.c, 2);
} }
updateTile(spot.r, spot.c, 2);
return true; return true;
} }
/* safe playSound */ /* Safe playSound */
function playSound(soundObj) { function playSound(soundObj) {
try { try {
soundObj.currentTime = 0; soundObj.currentTime = 0;
@ -167,7 +227,7 @@ function playSound(soundObj) {
} }
/* ------------------------ /* ------------------------
Movement helpers (logic preserved) Movement Logic
------------------------ */ ------------------------ */
function filterZero(row) { function filterZero(row) {
return row.filter(n => n !== 0); return row.filter(n => n !== 0);
@ -176,6 +236,7 @@ function filterZero(row) {
function slide(row) { function slide(row) {
row = filterZero(row); row = filterZero(row);
let mergedThisMove = false; let mergedThisMove = false;
let mergedPositions = []; // Track posisi yang merge
for (let i = 0; i < row.length - 1; i++) { for (let i = 0; i < row.length - 1; i++) {
if (row[i] === row[i + 1]) { if (row[i] === row[i + 1]) {
@ -183,129 +244,225 @@ function slide(row) {
playSound(audio.merge); playSound(audio.merge);
if (navigator.vibrate) navigator.vibrate(28); if (navigator.vibrate) navigator.vibrate(28);
score += row[i]; currentScore += row[i];
row[i + 1] = 0; row[i + 1] = 0;
mergedThisMove = true; mergedThisMove = true;
mergedPositions.push(i); // Simpan posisi merge
} }
} }
row = filterZero(row); row = filterZero(row);
while (row.length < 4) row.push(0); while (row.length < 4) row.push(0);
return { row, merged: mergedThisMove }; return { row, merged: mergedThisMove, mergedPositions };
} }
function arraysEqual(a, b) { function arraysEqual(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i]); return a.length === b.length && a.every((v, i) => v === b[i]);
} }
/* Move functions with Interactive Effects */
function moveLeft() { function moveLeft() {
let moved = false; let moved = false;
let mergedCells = [];
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
const { row: newRow } = slide(board[r]); const { row: newRow, mergedPositions } = slide(board[r]);
if (!arraysEqual(newRow, board[r])) moved = true; if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow; board[r] = newRow;
// Track merged cells untuk animasi
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(c => {
mergedCells.push({ r, c });
});
}
}
if (moved) {
refreshBoard();
// Trigger merge animation
mergedCells.forEach(cell => {
triggerMergeEffect(cell.r, cell.c);
});
} }
if (moved) updateAfterMove();
return moved; return moved;
} }
function moveRight() { function moveRight() {
let moved = false; let moved = false;
let mergedCells = [];
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
let reversed = [...board[r]].reverse(); let reversed = [...board[r]].reverse();
const { row: slid } = slide(reversed); const { row: slid, mergedPositions } = slide(reversed);
let newRow = slid.reverse(); let newRow = slid.reverse();
if (!arraysEqual(newRow, board[r])) moved = true; if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow; board[r] = newRow;
// Track merged cells
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(pos => {
const c = 3 - pos; // Reverse position
mergedCells.push({ r, c });
});
}
}
if (moved) {
refreshBoard();
mergedCells.forEach(cell => {
triggerMergeEffect(cell.r, cell.c);
});
} }
if (moved) updateAfterMove();
return moved; return moved;
} }
function moveUp() { function moveUp() {
let moved = false; let moved = false;
let mergedCells = [];
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
const col = [board[0][c], board[1][c], board[2][c], board[3][c]]; const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
const { row: newCol } = slide(col); const { row: newCol, mergedPositions } = slide(col);
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true; if (board[r][c] !== newCol[r]) moved = true;
board[r][c] = newCol[r]; board[r][c] = newCol[r];
} }
// Track merged cells
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(r => {
mergedCells.push({ r, c });
});
}
}
if (moved) {
refreshBoard();
mergedCells.forEach(cell => {
triggerMergeEffect(cell.r, cell.c);
});
} }
if (moved) updateAfterMove();
return moved; return moved;
} }
function moveDown() { function moveDown() {
let moved = false; let moved = false;
let mergedCells = [];
for (let c = 0; c < 4; c++) { for (let c = 0; c < 4; c++) {
const col = [board[3][c], board[2][c], board[1][c], board[0][c]]; const col = [board[3][c], board[2][c], board[1][c], board[0][c]];
const { row: slid } = slide(col); const { row: slid, mergedPositions } = slide(col);
const newCol = slid.reverse(); const newCol = slid.reverse();
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true; if (board[r][c] !== newCol[r]) moved = true;
board[r][c] = newCol[r]; board[r][c] = newCol[r];
} }
// Track merged cells
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(pos => {
const r = 3 - pos; // Reverse position
mergedCells.push({ r, c });
});
}
}
if (moved) {
refreshBoard();
mergedCells.forEach(cell => {
triggerMergeEffect(cell.r, cell.c);
});
} }
if (moved) updateAfterMove();
return moved; return moved;
} }
/* after move: refresh and reset lastMoveDir after small delay */
function updateAfterMove() {
// apply merge glow to merged tiles (scan for high values that were recently created)
refreshBoard();
updateScore();
// schedule dropping lastMoveDir after small delay so new tiles animate in direction
setTimeout(() => { lastMoveDir = null; }, 180);
}
/* ------------------------ /* ------------------------
Input handling (adds lastMoveDir + invalid-move shake) Input Handling - WITH WASD SUPPORT
------------------------ */ ------------------------ */
function handleKey(e) { function handleKey(e) {
if (isMoving) return;
let moved = false; let moved = false;
if (e.key === "ArrowLeft") { lastMoveDir = "left"; moved = moveLeft(); }
else if (e.key === "ArrowRight") { lastMoveDir = "right"; moved = moveRight(); } // Arrow Keys
else if (e.key === "ArrowUp") { lastMoveDir = "up"; moved = moveUp(); } if (e.key === "ArrowLeft") {
else if (e.key === "ArrowDown") { lastMoveDir = "down"; moved = moveDown(); } e.preventDefault();
moved = moveLeft();
}
else if (e.key === "ArrowRight") {
e.preventDefault();
moved = moveRight();
}
else if (e.key === "ArrowUp") {
e.preventDefault();
moved = moveUp();
}
else if (e.key === "ArrowDown") {
e.preventDefault();
moved = moveDown();
}
// WASD Keys
else if (e.key === "a" || e.key === "A") {
e.preventDefault();
moved = moveLeft();
}
else if (e.key === "d" || e.key === "D") {
e.preventDefault();
moved = moveRight();
}
else if (e.key === "w" || e.key === "W") {
e.preventDefault();
moved = moveUp();
}
else if (e.key === "s" || e.key === "S") {
e.preventDefault();
moved = moveDown();
}
if (moved) { if (moved) {
// add tile + subtle delay so new tile animates from direction isMoving = true;
setTimeout(() => { setTimeout(() => {
addNewTile(); const added = addNewTile();
refreshBoard(); if (!added || !canMove()) {
}, 70); setTimeout(() => showGameOver(), 300);
}
isMoving = false;
}, 100);
} else { } else {
// show board shake
const b = document.getElementById("board"); const b = document.getElementById("board");
if (b) { if (b) {
b.classList.add("shake"); b.classList.add("shake");
setTimeout(()=>b.classList.remove("shake"), 360); setTimeout(()=>b.classList.remove("shake"), 400);
} }
} }
} }
/* ------------------------ /* Check if any move is possible */
Restart & home function canMove() {
------------------------ */ for (let r = 0; r < 4; r++) {
function restartGame() { for (let c = 0; c < 4; c++) {
setupBoard(); if (board[r][c] === 0) return true;
addNewTile(); }
addNewTile(); }
refreshBoard();
} for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
function goHome() { const current = board[r][c];
try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} if (c < 3 && board[r][c + 1] === current) return true;
window.location.href = "Homepage.html"; if (r < 3 && board[r + 1][c] === current) return true;
}
}
return false;
} }
/* ------------------------ /* ------------------------
Touch swipe Touch Swipe
------------------------ */ ------------------------ */
let touchStartX = 0; let touchStartX = 0;
let touchStartY = 0; let touchStartY = 0;
document.addEventListener("touchstart", function (e) { document.addEventListener("touchstart", function (e) {
const t = e.touches[0]; const t = e.touches[0];
touchStartX = t.clientX; touchStartX = t.clientX;
@ -313,195 +470,234 @@ document.addEventListener("touchstart", function (e) {
}, { passive: true }); }, { passive: true });
document.addEventListener("touchend", function (e) { document.addEventListener("touchend", function (e) {
if (isMoving) return;
const t = e.changedTouches[0]; const t = e.changedTouches[0];
const dx = t.clientX - touchStartX; const dx = t.clientX - touchStartX;
const dy = t.clientY - touchStartY; const dy = t.clientY - touchStartY;
let moved = false;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) { if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
if (dx > 0) { lastMoveDir = "right"; moveRight() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } if (dx > 0) {
else { lastMoveDir = "left"; moveLeft() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } moved = moveRight();
} else {
moved = moveLeft();
}
} else if (Math.abs(dy) > 30) { } else if (Math.abs(dy) > 30) {
if (dy > 0) { lastMoveDir = "down"; moveDown() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } if (dy > 0) {
else { lastMoveDir = "up"; moveUp() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } moved = moveDown();
} else {
moved = moveUp();
}
}
if (moved) {
isMoving = true;
setTimeout(() => {
const added = addNewTile();
if (!added || !canMove()) {
setTimeout(() => showGameOver(), 300);
}
isMoving = false;
}, 100);
} }
}, { passive: true }); }, { passive: true });
/* ------------------------ /* ------------------------
Ambient cursor light + merge particles Game Controls
------------------------ */
function restartGame() {
hideGameOver();
resetScore();
setupBoard();
addNewTile();
addNewTile();
refreshBoard();
isMoving = false;
}
function playAgain() {
restartGame();
}
function goHome() {
try {
audio.bg.pause();
audio.bg.currentTime = 0;
} catch (e) {}
window.location.href = "Homepage.html";
}
/* ------------------------
Game Over Modal
------------------------ */
function showGameOver() {
const finalScore = currentScore;
const isNewHighScore = finalScore >= bestScore && finalScore > 0;
const finalScoreEl = document.getElementById('final-score');
if (finalScoreEl) {
finalScoreEl.textContent = finalScore;
}
const newHighScoreBadge = document.getElementById('new-high-score-badge');
const bestScoreDisplay = document.getElementById('best-score-display');
if (isNewHighScore) {
if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-flex';
if (bestScoreDisplay) bestScoreDisplay.style.display = 'none';
} else {
if (newHighScoreBadge) newHighScoreBadge.style.display = 'none';
if (bestScoreDisplay) bestScoreDisplay.style.display = 'block';
const modalBestScore = document.getElementById('modal-best-score');
if (modalBestScore) modalBestScore.textContent = bestScore;
}
const gameOverOverlay = document.getElementById('game-over-overlay');
if (gameOverOverlay) {
gameOverOverlay.style.display = 'flex';
}
}
function hideGameOver() {
const gameOverOverlay = document.getElementById('game-over-overlay');
if (gameOverOverlay) {
gameOverOverlay.style.display = 'none';
}
}
/* ------------------------
Visual Effects (Simplified)
------------------------ */ ------------------------ */
function setupAmbientCursor() { function setupAmbientCursor() {
const container = document.querySelector(".particles"); // Cursor light effect removed for performance
if (!container) return; // Keeping function for compatibility
// create a subtle cursor-follow blob
const cursor = document.createElement("div");
cursor.className = "cursor-light";
container.appendChild(cursor);
let lastX = window.innerWidth/2, lastY = window.innerHeight/2;
document.addEventListener("mousemove", (e) => {
lastX = e.clientX; lastY = e.clientY;
cursor.style.left = lastX + "px";
cursor.style.top = lastY + "px";
});
// small periodic motion for background
setInterval(() => {
cursor.style.opacity = (0.4 + Math.random()*0.35).toString();
}, 900);
} }
/* spawn merge particles at tile center */ /* =============================================
function spawnMergeParticles(row, col, colorHex="#00eaff") { INTERACTIVE MERGE EFFECTS
const container = document.body; ============================================= */
const boardRect = document.getElementById("board").getBoundingClientRect();
const tileEl = document.getElementById(`${row}-${col}`);
if (!tileEl) return;
const tileRect = tileEl.getBoundingClientRect(); function triggerMergeEffect(row, col) {
const cx = tileRect.left + tileRect.width/2; const tile = document.getElementById(`${row}-${col}`);
const cy = tileRect.top + tileRect.height/2; if (!tile) return;
const particles = []; // Add merge class for CSS animation
const count = 10; tile.classList.add('merge');
for (let i = 0; i < count; i++) { setTimeout(() => tile.classList.remove('merge'), 300);
const p = document.createElement("div");
p.className = "merge-particle"; // Create particle burst effect
p.style.background = colorHex; createParticleBurst(tile);
p.style.left = (cx - 6) + "px";
p.style.top = (cy - 6) + "px"; // Add glow pulse
p.style.opacity = "1"; tile.style.boxShadow = '0 0 40px currentColor';
p.style.transform = "translate(0,0) scale(1)"; setTimeout(() => {
document.body.appendChild(p); tile.style.boxShadow = '';
particles.push(p); }, 300);
// random flight vector
const angle = Math.random() * Math.PI * 2;
const dist = 24 + Math.random()*36;
const tx = Math.cos(angle) * dist;
const ty = Math.sin(angle) * dist;
const rot = (Math.random() * 360)|0;
p.animate([
{ transform: `translate(0,0) rotate(0deg) scale(1)`, opacity: 1 },
{ transform: `translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(0.6)`, opacity: 0 }
], {
duration: 420 + Math.random()*240,
easing: "cubic-bezier(.2,.8,.2,1)",
fill: "forwards"
});
// cleanup
setTimeout(()=>{ try{ p.remove(); }catch(e){} }, 800 + Math.random()*400);
}
} }
/* ------------------------ function createParticleBurst(tileElement) {
Optional: call spawn on merges const rect = tileElement.getBoundingClientRect();
We don't track exact merge positions in slide() local scope here, const centerX = rect.left + rect.width / 2;
but we can detect new larger tiles after move vs before and spawn particles. const centerY = rect.top + rect.height / 2;
------------------------ */
function spawnMergesFromDiff(prev, next) { // Get tile color
// prev & next are 4x4 arrays const tileValue = parseInt(tileElement.textContent);
for (let r = 0; r < 4; r++) { const tileColor = getTileColor(tileValue);
for (let c = 0; c < 4; c++) {
if (next[r][c] > 0 && prev[r][c] !== next[r][c]) { // Create 8-12 particles
// if new value appears that wasn't same in prev -> likely merged or moved; if it's > 2 we spawn small effect const particleCount = 8 + Math.floor(Math.random() * 5);
if (next[r][c] >= 4) {
spawnMergeParticles(r, c, chooseColorForValue(next[r][c])); for (let i = 0; i < particleCount; i++) {
const tileEl = document.getElementById(`${r}-${c}`); const particle = document.createElement('div');
if (tileEl) { particle.className = 'merge-particle';
tileEl.classList.add("merge"); particle.style.left = centerX + 'px';
setTimeout(()=>tileEl.classList.remove("merge"), 260); particle.style.top = centerY + 'px';
} particle.style.background = tileColor;
}
document.body.appendChild(particle);
// Random direction
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;
// Animate particle
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();
} }
// Add score popup
createScorePopup(centerX, centerY, tileValue);
} }
/* choose nice color for particle based on value */ function createScorePopup(x, y, score) {
function chooseColorForValue(n) { const popup = document.createElement('div');
if (n >= 2048) return "#ffd700"; popup.className = 'score-popup';
if (n >= 1024) return "#00ffaa"; popup.textContent = '+' + score;
if (n >= 512) return "#ff00aa"; popup.style.left = x + 'px';
if (n >= 128) return "#5f00ff"; popup.style.top = y + 'px';
if (n >= 32) return "#ffaa00"; popup.style.position = 'fixed';
return "#00eaff"; 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();
} }
/* We'll wrap move functions to produce prev snapshot, then spawn particles for merges detected */ function getTileColor(value) {
function cloneBoard(b) { const colors = {
const out = []; 2: '#00eaff',
for (let r = 0; r < 4; r++) out.push([...b[r]]); 4: '#00ff99',
return out; 8: '#ff00ff',
} 16: '#ff0066',
32: '#ffaa00',
/* Override move functions to spawn particles after move */ 64: '#ff0000',
function moveLeft() { 128: '#5f00ff',
const prev = cloneBoard(board); 256: '#00ffea',
let moved = false; 512: '#ff00aa',
for (let r = 0; r < 4; r++) { 1024: '#00ffaa',
const { row: newRow } = slide(board[r]); 2048: '#ffd700'
if (!arraysEqual(newRow, board[r])) moved = true; };
board[r] = newRow; return colors[value] || '#00eaff';
}
if (moved) {
spawnMergesFromDiff(prev, board);
updateAfterMove();
}
return moved;
}
function moveRight() {
const prev = cloneBoard(board);
let moved = false;
for (let r = 0; r < 4; r++) {
let reversed = [...board[r]].reverse();
const { row: slid } = slide(reversed);
let newRow = slid.reverse();
if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow;
}
if (moved) {
spawnMergesFromDiff(prev, board);
updateAfterMove();
}
return moved;
}
function moveUp() {
const prev = cloneBoard(board);
let moved = false;
for (let c = 0; c < 4; c++) {
const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
const { row: newCol } = slide(col);
for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true;
board[r][c] = newCol[r];
}
}
if (moved) {
spawnMergesFromDiff(prev, board);
updateAfterMove();
}
return moved;
}
function moveDown() {
const prev = cloneBoard(board);
let moved = false;
for (let c = 0; c < 4; c++) {
const col = [board[3][c], board[2][c], board[1][c], board[0][c]];
const { row: slid } = 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];
}
}
if (moved) {
spawnMergesFromDiff(prev, board);
updateAfterMove();
}
return moved;
} }
/* ------------------------ /* ------------------------
End of file End of File
------------------------ */ ------------------------ */