508 lines
14 KiB
JavaScript
508 lines
14 KiB
JavaScript
/* 2048.js — Enhanced with animations, particles, and glows
|
|
Replace previous 2048.js content with this file.
|
|
*/
|
|
|
|
/* ------------------------
|
|
State & audio (kept)
|
|
------------------------ */
|
|
let board = [];
|
|
let score = 0;
|
|
let lastMoveDir = null; // 'left','right','up','down' or null
|
|
|
|
// --- Audio setup ---
|
|
const audio = {
|
|
bg: new Audio("bgmusic.mp3"),
|
|
pop: new Audio("pop.mp3"),
|
|
merge: new Audio("merge.wav")
|
|
};
|
|
audio.bg.volume = 0.25;
|
|
audio.pop.volume = 0.9;
|
|
audio.merge.volume = 0.9;
|
|
audio.bg.loop = true;
|
|
|
|
function tryPlayBg() {
|
|
audio.bg.play().catch(() => {
|
|
const unlock = () => {
|
|
audio.bg.play().catch(()=>{});
|
|
window.removeEventListener("keydown", unlock);
|
|
window.removeEventListener("click", unlock);
|
|
};
|
|
window.addEventListener("keydown", unlock, { once: true });
|
|
window.addEventListener("click", unlock, { once: true });
|
|
});
|
|
}
|
|
|
|
/* ------------------------
|
|
DOM ready
|
|
------------------------ */
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
setupBoard();
|
|
addNewTile();
|
|
addNewTile();
|
|
tryPlayBg();
|
|
document.addEventListener("keydown", handleKey);
|
|
setupAmbientCursor();
|
|
});
|
|
|
|
/* ------------------------
|
|
Setup & rendering
|
|
------------------------ */
|
|
function setupBoard() {
|
|
board = [];
|
|
score = 0;
|
|
updateScore();
|
|
|
|
const container = document.getElementById("board");
|
|
if (!container) {
|
|
console.error("Board element not found (#board).");
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = "";
|
|
for (let r = 0; r < 4; r++) {
|
|
board[r] = [];
|
|
for (let c = 0; c < 4; c++) {
|
|
board[r][c] = 0;
|
|
const tile = document.createElement("div");
|
|
tile.id = `${r}-${c}`;
|
|
tile.className = "tile";
|
|
container.appendChild(tile);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* update single tile visual with small entrance based on last move */
|
|
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 */
|
|
function refreshBoard() {
|
|
for (let r = 0; r < 4; r++) {
|
|
for (let c = 0; c < 4; c++) {
|
|
updateTile(r, c, board[r][c]);
|
|
}
|
|
}
|
|
updateScore();
|
|
}
|
|
|
|
/* score */
|
|
function updateScore() {
|
|
const el = document.getElementById("score");
|
|
if (el) el.textContent = score;
|
|
}
|
|
|
|
/* add new tile with pop animation */
|
|
function addNewTile() {
|
|
const empty = [];
|
|
for (let r = 0; r < 4; r++) {
|
|
for (let c = 0; c < 4; c++) {
|
|
if (board[r][c] === 0) empty.push({ r, c });
|
|
}
|
|
}
|
|
|
|
if (empty.length === 0) return false;
|
|
|
|
const spot = empty[Math.floor(Math.random() * empty.length)];
|
|
board[spot.r][spot.c] = 2;
|
|
|
|
const tile = document.getElementById(`${spot.r}-${spot.c}`);
|
|
if (tile) {
|
|
tile.classList.add("new");
|
|
playSound(audio.pop);
|
|
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);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* safe playSound */
|
|
function playSound(soundObj) {
|
|
try {
|
|
soundObj.currentTime = 0;
|
|
soundObj.play().catch(() => {});
|
|
} catch (e) {}
|
|
}
|
|
|
|
/* ------------------------
|
|
Movement helpers (logic preserved)
|
|
------------------------ */
|
|
function filterZero(row) {
|
|
return row.filter(n => n !== 0);
|
|
}
|
|
|
|
function slide(row) {
|
|
row = filterZero(row);
|
|
let mergedThisMove = false;
|
|
|
|
for (let i = 0; i < row.length - 1; i++) {
|
|
if (row[i] === row[i + 1]) {
|
|
row[i] = row[i] * 2;
|
|
playSound(audio.merge);
|
|
if (navigator.vibrate) navigator.vibrate(28);
|
|
|
|
score += row[i];
|
|
row[i + 1] = 0;
|
|
mergedThisMove = true;
|
|
}
|
|
}
|
|
|
|
row = filterZero(row);
|
|
while (row.length < 4) row.push(0);
|
|
return { row, merged: mergedThisMove };
|
|
}
|
|
|
|
function arraysEqual(a, b) {
|
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
}
|
|
|
|
function moveLeft() {
|
|
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) updateAfterMove();
|
|
return moved;
|
|
}
|
|
|
|
function moveRight() {
|
|
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) updateAfterMove();
|
|
return moved;
|
|
}
|
|
|
|
function moveUp() {
|
|
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) updateAfterMove();
|
|
return moved;
|
|
}
|
|
|
|
function moveDown() {
|
|
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) 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)
|
|
------------------------ */
|
|
function handleKey(e) {
|
|
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(); }
|
|
|
|
if (moved) {
|
|
// add tile + subtle delay so new tile animates from direction
|
|
setTimeout(() => {
|
|
addNewTile();
|
|
refreshBoard();
|
|
}, 70);
|
|
} else {
|
|
// show board shake
|
|
const b = document.getElementById("board");
|
|
if (b) {
|
|
b.classList.add("shake");
|
|
setTimeout(()=>b.classList.remove("shake"), 360);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------
|
|
Restart & home
|
|
------------------------ */
|
|
function restartGame() {
|
|
setupBoard();
|
|
addNewTile();
|
|
addNewTile();
|
|
refreshBoard();
|
|
}
|
|
|
|
function goHome() {
|
|
try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {}
|
|
window.location.href = "Homepage.html";
|
|
}
|
|
|
|
/* ------------------------
|
|
Touch swipe
|
|
------------------------ */
|
|
let touchStartX = 0;
|
|
let touchStartY = 0;
|
|
document.addEventListener("touchstart", function (e) {
|
|
const t = e.touches[0];
|
|
touchStartX = t.clientX;
|
|
touchStartY = t.clientY;
|
|
}, { passive: true });
|
|
|
|
document.addEventListener("touchend", function (e) {
|
|
const t = e.changedTouches[0];
|
|
const dx = t.clientX - touchStartX;
|
|
const dy = t.clientY - touchStartY;
|
|
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); }
|
|
} else if (Math.abs(dy) > 30) {
|
|
if (dy > 0) { lastMoveDir = "down"; moveDown() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); }
|
|
else { lastMoveDir = "up"; moveUp() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); }
|
|
}
|
|
}, { passive: true });
|
|
|
|
/* ------------------------
|
|
Ambient cursor light + merge particles
|
|
------------------------ */
|
|
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);
|
|
}
|
|
|
|
/* 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;
|
|
|
|
const tileRect = tileEl.getBoundingClientRect();
|
|
const cx = tileRect.left + tileRect.width/2;
|
|
const cy = tileRect.top + tileRect.height/2;
|
|
|
|
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);
|
|
|
|
// random flight vector
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 24 + Math.random()*36;
|
|
const tx = Math.cos(angle) * dist;
|
|
const ty = Math.sin(angle) * dist;
|
|
const rot = (Math.random() * 360)|0;
|
|
p.animate([
|
|
{ transform: `translate(0,0) rotate(0deg) scale(1)`, opacity: 1 },
|
|
{ transform: `translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(0.6)`, opacity: 0 }
|
|
], {
|
|
duration: 420 + Math.random()*240,
|
|
easing: "cubic-bezier(.2,.8,.2,1)",
|
|
fill: "forwards"
|
|
});
|
|
|
|
// cleanup
|
|
setTimeout(()=>{ try{ p.remove(); }catch(e){} }, 800 + Math.random()*400);
|
|
}
|
|
}
|
|
|
|
/* ------------------------
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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
|
|
------------------------ */
|