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

176
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">
</head>
<body>
<!-- Background Effects -->
<div class="particles" aria-hidden="true"></div> <div class="particles" aria-hidden="true"></div>
<div class="starfield"></div> <div class="starfield" aria-hidden="true"></div>
<div class="cursor-light"></div> <div class="cursor-light" aria-hidden="true"></div>
<h1>2048</h1>
<div id="top-menu"> <!-- Top Right Controls -->
<button class="btn" onclick="restartGame()">Restart</button> <div class="top-controls">
<button class="btn" onclick="goHome()">Home</button> <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> </div>
<hr> <!-- Game Container -->
<h2>Score: <span id="score">0</span></h2> <div class="game-container">
<div id="board"> <!-- 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>
<div class="score-box">
<div class="score-label">HIGH SCORE</div>
<div class="score-value" id="best-score">0</div>
</div>
</div>
</div>
<!-- Game Board -->
<div id="board"></div>
</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>
<!-- 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> <script src="2048.js"></script>
</body> </body>
</html> </html>

732
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();
}
function goHome() { for (let r = 0; r < 4; r++) {
try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} for (let c = 0; c < 4; c++) {
window.location.href = "Homepage.html"; 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 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";
p.style.background = colorHex;
p.style.left = (cx - 6) + "px";
p.style.top = (cy - 6) + "px";
p.style.opacity = "1";
p.style.transform = "translate(0,0) scale(1)";
document.body.appendChild(p);
particles.push(p);
// random flight vector // Create particle burst effect
const angle = Math.random() * Math.PI * 2; createParticleBurst(tile);
const dist = 24 + Math.random()*36;
const tx = Math.cos(angle) * dist; // Add glow pulse
const ty = Math.sin(angle) * dist; tile.style.boxShadow = '0 0 40px currentColor';
const rot = (Math.random() * 360)|0; setTimeout(() => {
p.animate([ tile.style.boxShadow = '';
{ transform: `translate(0,0) rotate(0deg) scale(1)`, opacity: 1 }, }, 300);
{ transform: `translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(0.6)`, opacity: 0 } }
function createParticleBurst(tileElement) {
const rect = tileElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Get tile color
const tileValue = parseInt(tileElement.textContent);
const tileColor = getTileColor(tileValue);
// Create 8-12 particles
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);
// 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: 420 + Math.random()*240, duration: 500 + Math.random() * 200,
easing: "cubic-bezier(.2,.8,.2,1)", easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
fill: "forwards" }).onfinish = () => particle.remove();
});
// cleanup
setTimeout(()=>{ try{ p.remove(); }catch(e){} }, 800 + Math.random()*400);
} }
// Add score popup
createScorePopup(centerX, centerY, tileValue);
}
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';
} }
/* ------------------------ /* ------------------------
Optional: call spawn on merges End of File
We don't track exact merge positions in slide() local scope here,
but we can detect new larger tiles after move vs before and spawn particles.
------------------------ */
function spawnMergesFromDiff(prev, next) {
// prev & next are 4x4 arrays
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
if (next[r][c] > 0 && prev[r][c] !== next[r][c]) {
// if new value appears that wasn't same in prev -> likely merged or moved; if it's > 2 we spawn small effect
if (next[r][c] >= 4) {
spawnMergeParticles(r, c, chooseColorForValue(next[r][c]));
const tileEl = document.getElementById(`${r}-${c}`);
if (tileEl) {
tileEl.classList.add("merge");
setTimeout(()=>tileEl.classList.remove("merge"), 260);
}
}
}
}
}
}
/* choose nice color for particle based on value */
function chooseColorForValue(n) {
if (n >= 2048) return "#ffd700";
if (n >= 1024) return "#00ffaa";
if (n >= 512) return "#ff00aa";
if (n >= 128) return "#5f00ff";
if (n >= 32) return "#ffaa00";
return "#00eaff";
}
/* We'll wrap move functions to produce prev snapshot, then spawn particles for merges detected */
function cloneBoard(b) {
const out = [];
for (let r = 0; r < 4; r++) out.push([...b[r]]);
return out;
}
/* Override move functions to spawn particles after move */
function moveLeft() {
const prev = cloneBoard(board);
let moved = false;
for (let r = 0; r < 4; r++) {
const { row: newRow } = slide(board[r]);
if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow;
}
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
------------------------ */ ------------------------ */