2048 update
This commit is contained in:
parent
07be893964
commit
e2ba499ce6
188
2048.html
188
2048.html
@ -1,26 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>2048</title>
|
||||
<link rel="stylesheet" href="2048.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- ADD: ambient particle / neon background layer -->
|
||||
<div class="particles" aria-hidden="true"></div>
|
||||
<div class="starfield"></div>
|
||||
<div class="cursor-light"></div>
|
||||
<h1>2048</h1>
|
||||
<div id="top-menu">
|
||||
<button class="btn" onclick="restartGame()">Restart</button>
|
||||
<button class="btn" onclick="goHome()">Home</button>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>2048</title>
|
||||
<link rel="stylesheet" href="2048.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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="starfield" aria-hidden="true"></div>
|
||||
<div class="cursor-light" aria-hidden="true"></div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<hr>
|
||||
<h2>Score: <span id="score">0</span></h2>
|
||||
<div id="board">
|
||||
<!-- 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>
|
||||
<script src="2048.js"></script>
|
||||
</body>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
718
2048.js
718
2048.js
@ -1,15 +1,17 @@
|
||||
/* 2048.js — Enhanced with animations, particles, and glows
|
||||
Replace previous 2048.js content with this file.
|
||||
*/
|
||||
/* 2048.js — Complete Version with WASD + Interactive Merge Effects */
|
||||
|
||||
/* ------------------------
|
||||
State & audio (kept)
|
||||
State & Variables
|
||||
------------------------ */
|
||||
let board = [];
|
||||
let score = 0;
|
||||
let lastMoveDir = null; // 'left','right','up','down' or null
|
||||
let currentScore = 0;
|
||||
let bestScore = parseInt(localStorage.getItem('bestScore2048')) || 0;
|
||||
let lastMoveDir = null;
|
||||
let isMoving = false;
|
||||
|
||||
// --- Audio setup ---
|
||||
/* ------------------------
|
||||
Audio Setup
|
||||
------------------------ */
|
||||
const audio = {
|
||||
bg: new Audio("bgmusic.mp3"),
|
||||
pop: new Audio("pop.mp3"),
|
||||
@ -33,24 +35,90 @@ function tryPlayBg() {
|
||||
}
|
||||
|
||||
/* ------------------------
|
||||
DOM ready
|
||||
DOM Ready
|
||||
------------------------ */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
updateBestScoreDisplay();
|
||||
setupBoard();
|
||||
addNewTile();
|
||||
addNewTile();
|
||||
tryPlayBg();
|
||||
document.addEventListener("keydown", handleKey);
|
||||
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() {
|
||||
board = [];
|
||||
score = 0;
|
||||
updateScore();
|
||||
currentScore = 0;
|
||||
updateScoreDisplay();
|
||||
|
||||
const container = document.getElementById("board");
|
||||
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) {
|
||||
const tile = document.getElementById(`${row}-${col}`);
|
||||
if (!tile) return;
|
||||
|
||||
// reset classes except base .tile
|
||||
tile.className = "tile";
|
||||
|
||||
// ensure previous transforms cleared
|
||||
tile.style.transform = "";
|
||||
tile.style.opacity = "";
|
||||
|
||||
if (num > 0) {
|
||||
tile.textContent = 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 {
|
||||
tile.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
/* refresh whole board */
|
||||
/* Refresh whole board */
|
||||
function refreshBoard() {
|
||||
for (let r = 0; r < 4; r++) {
|
||||
for (let c = 0; c < 4; c++) {
|
||||
updateTile(r, c, board[r][c]);
|
||||
}
|
||||
}
|
||||
updateScore();
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
/* score */
|
||||
function updateScore() {
|
||||
const el = document.getElementById("score");
|
||||
if (el) el.textContent = score;
|
||||
/* ------------------------
|
||||
Score Management
|
||||
------------------------ */
|
||||
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() {
|
||||
const empty = [];
|
||||
for (let r = 0; r < 4; r++) {
|
||||
@ -147,18 +212,13 @@ function addNewTile() {
|
||||
if (tile) {
|
||||
tile.classList.add("new");
|
||||
playSound(audio.pop);
|
||||
tile.addEventListener("animationend", function handler() {
|
||||
tile.classList.remove("new");
|
||||
tile.removeEventListener("animationend", handler);
|
||||
});
|
||||
updateTile(spot.r, spot.c, 2);
|
||||
} else {
|
||||
updateTile(spot.r, spot.c, 2);
|
||||
setTimeout(() => tile.classList.remove("new"), 300);
|
||||
}
|
||||
updateTile(spot.r, spot.c, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* safe playSound */
|
||||
/* Safe playSound */
|
||||
function playSound(soundObj) {
|
||||
try {
|
||||
soundObj.currentTime = 0;
|
||||
@ -167,7 +227,7 @@ function playSound(soundObj) {
|
||||
}
|
||||
|
||||
/* ------------------------
|
||||
Movement helpers (logic preserved)
|
||||
Movement Logic
|
||||
------------------------ */
|
||||
function filterZero(row) {
|
||||
return row.filter(n => n !== 0);
|
||||
@ -176,6 +236,7 @@ function filterZero(row) {
|
||||
function slide(row) {
|
||||
row = filterZero(row);
|
||||
let mergedThisMove = false;
|
||||
let mergedPositions = []; // Track posisi yang merge
|
||||
|
||||
for (let i = 0; i < row.length - 1; i++) {
|
||||
if (row[i] === row[i + 1]) {
|
||||
@ -183,129 +244,225 @@ function slide(row) {
|
||||
playSound(audio.merge);
|
||||
if (navigator.vibrate) navigator.vibrate(28);
|
||||
|
||||
score += row[i];
|
||||
currentScore += row[i];
|
||||
row[i + 1] = 0;
|
||||
mergedThisMove = true;
|
||||
mergedPositions.push(i); // Simpan posisi merge
|
||||
}
|
||||
}
|
||||
|
||||
row = filterZero(row);
|
||||
while (row.length < 4) row.push(0);
|
||||
return { row, merged: mergedThisMove };
|
||||
return { row, merged: mergedThisMove, mergedPositions };
|
||||
}
|
||||
|
||||
function arraysEqual(a, b) {
|
||||
return a.length === b.length && a.every((v, i) => v === b[i]);
|
||||
}
|
||||
|
||||
/* Move functions with Interactive Effects */
|
||||
function moveLeft() {
|
||||
let moved = false;
|
||||
let mergedCells = [];
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
function moveRight() {
|
||||
let moved = false;
|
||||
let mergedCells = [];
|
||||
|
||||
for (let r = 0; r < 4; r++) {
|
||||
let reversed = [...board[r]].reverse();
|
||||
const { row: slid } = slide(reversed);
|
||||
const { row: slid, mergedPositions } = slide(reversed);
|
||||
let newRow = slid.reverse();
|
||||
if (!arraysEqual(newRow, board[r])) moved = true;
|
||||
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;
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
let moved = false;
|
||||
let mergedCells = [];
|
||||
|
||||
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);
|
||||
const { row: newCol, mergedPositions } = slide(col);
|
||||
for (let r = 0; r < 4; r++) {
|
||||
if (board[r][c] !== newCol[r]) moved = true;
|
||||
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;
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
let moved = false;
|
||||
let mergedCells = [];
|
||||
|
||||
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 { row: slid, mergedPositions } = 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];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
if (isMoving) return;
|
||||
|
||||
let moved = false;
|
||||
if (e.key === "ArrowLeft") { lastMoveDir = "left"; moved = moveLeft(); }
|
||||
else if (e.key === "ArrowRight") { lastMoveDir = "right"; moved = moveRight(); }
|
||||
else if (e.key === "ArrowUp") { lastMoveDir = "up"; moved = moveUp(); }
|
||||
else if (e.key === "ArrowDown") { lastMoveDir = "down"; moved = moveDown(); }
|
||||
|
||||
// Arrow Keys
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
moved = moveLeft();
|
||||
}
|
||||
else if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
moved = moveRight();
|
||||
}
|
||||
else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
moved = moveUp();
|
||||
}
|
||||
else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
moved = moveDown();
|
||||
}
|
||||
// WASD Keys
|
||||
else if (e.key === "a" || e.key === "A") {
|
||||
e.preventDefault();
|
||||
moved = moveLeft();
|
||||
}
|
||||
else if (e.key === "d" || e.key === "D") {
|
||||
e.preventDefault();
|
||||
moved = moveRight();
|
||||
}
|
||||
else if (e.key === "w" || e.key === "W") {
|
||||
e.preventDefault();
|
||||
moved = moveUp();
|
||||
}
|
||||
else if (e.key === "s" || e.key === "S") {
|
||||
e.preventDefault();
|
||||
moved = moveDown();
|
||||
}
|
||||
|
||||
if (moved) {
|
||||
// add tile + subtle delay so new tile animates from direction
|
||||
isMoving = true;
|
||||
setTimeout(() => {
|
||||
addNewTile();
|
||||
refreshBoard();
|
||||
}, 70);
|
||||
const added = addNewTile();
|
||||
if (!added || !canMove()) {
|
||||
setTimeout(() => showGameOver(), 300);
|
||||
}
|
||||
isMoving = false;
|
||||
}, 100);
|
||||
} else {
|
||||
// show board shake
|
||||
const b = document.getElementById("board");
|
||||
if (b) {
|
||||
b.classList.add("shake");
|
||||
setTimeout(()=>b.classList.remove("shake"), 360);
|
||||
setTimeout(()=>b.classList.remove("shake"), 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------
|
||||
Restart & home
|
||||
------------------------ */
|
||||
function restartGame() {
|
||||
setupBoard();
|
||||
addNewTile();
|
||||
addNewTile();
|
||||
refreshBoard();
|
||||
}
|
||||
/* Check if any move is possible */
|
||||
function canMove() {
|
||||
for (let r = 0; r < 4; r++) {
|
||||
for (let c = 0; c < 4; c++) {
|
||||
if (board[r][c] === 0) return true;
|
||||
}
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {}
|
||||
window.location.href = "Homepage.html";
|
||||
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
|
||||
Touch Swipe
|
||||
------------------------ */
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
|
||||
document.addEventListener("touchstart", function (e) {
|
||||
const t = e.touches[0];
|
||||
touchStartX = t.clientX;
|
||||
@ -313,195 +470,234 @@ document.addEventListener("touchstart", function (e) {
|
||||
}, { 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) { lastMoveDir = "right"; moveRight() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); }
|
||||
else { lastMoveDir = "left"; moveLeft() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); }
|
||||
if (dx > 0) {
|
||||
moved = moveRight();
|
||||
} else {
|
||||
moved = moveLeft();
|
||||
}
|
||||
} else if (Math.abs(dy) > 30) {
|
||||
if (dy > 0) { lastMoveDir = "down"; moveDown() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); }
|
||||
else { lastMoveDir = "up"; moveUp() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); }
|
||||
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 });
|
||||
|
||||
/* ------------------------
|
||||
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() {
|
||||
const container = document.querySelector(".particles");
|
||||
if (!container) return;
|
||||
|
||||
// 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);
|
||||
// Cursor light effect removed for performance
|
||||
// Keeping function for compatibility
|
||||
}
|
||||
|
||||
/* spawn merge particles at tile center */
|
||||
function spawnMergeParticles(row, col, colorHex="#00eaff") {
|
||||
const container = document.body;
|
||||
const boardRect = document.getElementById("board").getBoundingClientRect();
|
||||
const tileEl = document.getElementById(`${row}-${col}`);
|
||||
if (!tileEl) return;
|
||||
/* =============================================
|
||||
INTERACTIVE MERGE EFFECTS
|
||||
============================================= */
|
||||
|
||||
const tileRect = tileEl.getBoundingClientRect();
|
||||
const cx = tileRect.left + tileRect.width/2;
|
||||
const cy = tileRect.top + tileRect.height/2;
|
||||
function triggerMergeEffect(row, col) {
|
||||
const tile = document.getElementById(`${row}-${col}`);
|
||||
if (!tile) return;
|
||||
|
||||
const particles = [];
|
||||
const count = 10;
|
||||
for (let i = 0; i < count; i++) {
|
||||
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);
|
||||
// Add merge class for CSS animation
|
||||
tile.classList.add('merge');
|
||||
setTimeout(() => tile.classList.remove('merge'), 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"
|
||||
});
|
||||
// Create particle burst effect
|
||||
createParticleBurst(tile);
|
||||
|
||||
// cleanup
|
||||
setTimeout(()=>{ try{ p.remove(); }catch(e){} }, 800 + Math.random()*400);
|
||||
}
|
||||
// Add glow pulse
|
||||
tile.style.boxShadow = '0 0 40px currentColor';
|
||||
setTimeout(() => {
|
||||
tile.style.boxShadow = '';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/* ------------------------
|
||||
Optional: call spawn on merges
|
||||
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);
|
||||
}
|
||||
}
|
||||
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: 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 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";
|
||||
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();
|
||||
}
|
||||
|
||||
/* 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;
|
||||
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';
|
||||
}
|
||||
|
||||
/* ------------------------
|
||||
End of file
|
||||
End of File
|
||||
------------------------ */
|
||||
Loading…
x
Reference in New Issue
Block a user