2048 update
This commit is contained in:
parent
07be893964
commit
e2ba499ce6
176
2048.html
176
2048.html
@ -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
732
2048.js
@ -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
|
|
||||||
------------------------ */
|
------------------------ */
|
||||||
Loading…
x
Reference in New Issue
Block a user