/* 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 ------------------------ */