diff --git a/2048.css b/2048.css index c8a3573..88da4ad 100644 --- a/2048.css +++ b/2048.css @@ -1,104 +1,146 @@ +/* ====================== + GLOBAL +====================== */ body { - background-color: black; - color: white; /* opsional, biar teks tetap terlihat */ - font-family: Arial, Helvetica, sans-serif; + background: radial-gradient(circle at center, #0a0a0a, #000); + background-size: 300% 300%; + animation: bgMove 15s infinite alternate; + font-family: 'Poppins', sans-serif; + color: white; text-align: center; + padding-top: 20px; } -hr { - width: 500px; +/* Background animasi lembut */ +@keyframes bgMove { + 0% { background-position: 0% 30%; } + 50% { background-position: 50% 70%; } + 100% { background-position: 100% 30%; } } +h1 { + font-size: 40px; + font-weight: bold; + text-shadow: 0 0 20px #00eaff, 0 0 40px #0099ff; +} + +button { + padding: 12px 22px; + margin: 8px; + background: #111; + color: white; + border: 2px solid #00eaff; + border-radius: 10px; + cursor: pointer; + transition: 0.25s; + font-weight: bold; +} +button:hover { + box-shadow: 0 0 15px #00eaff; + transform: scale(1.05); +} + +#score { + color: #00eaff; + text-shadow: 0 0 10px #00eaff; + font-weight: bold; + font-size: 20px; +} + +/* ====================== + BOARD +====================== */ + + +/* ========================== + BOARD — MIRIP BORDER LOGIN +========================== */ + #board { - width: 400px; - height: 400px; - background-color: black; - margin: 0 auto; + width: 460px; + height: 460px; + margin: 35px auto; + padding: 20px; + display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); - gap: 5px; - padding: 10px; - box-sizing: border-box; - border: 2px solid red; - box-shadow: 0 0 20px red, 0 0 40px red, 0 0 60px red; - border-radius: 15px; + gap: 12px; + + background: rgba(30, 0, 50, 0.6); /* DALAM GELAP SAMA LOGIN */ + backdrop-filter: blur(10px); + + border: 2px solid rgba(0, 217, 255, 0.3); /* SAMA LOGIN */ + border-radius: 20px; + + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 40px rgba(0, 217, 255, 0.3), + inset 0 0 30px rgba(0, 217, 255, 0.1); + + animation: glowBorderBoard 3s ease-in-out infinite; } +/* Border berubah warna seperti login */ +@keyframes glowBorderBoard { + 0%, 100% { + border-color: rgba(0, 217, 255, 0.4); + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 40px rgba(0, 217, 255, 0.3), + inset 0 0 30px rgba(0, 217, 255, 0.1); + } + 50% { + border-color: rgba(255, 0, 255, 0.5); + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 50px rgba(255, 0, 255, 0.4), + inset 0 0 40px rgba(255, 0, 255, 0.15); + } +} + + + +/* ====================== + TILE +====================== */ .tile { - font-size: 40px; - font-weight: bold; + width: 100%; + height: 100%; + border-radius: 14px; display: flex; - justify-content: center; align-items: center; - background-color: white; - border-radius: 15px; -} + justify-content: center; + font-size: 32px; + font-weight: bold; -/* colored tiles */ - -.x2 { - background-color: #eee4da; - color: #727371; -} - -.x4 { - background-color: #ece0ca; - color: #727371; -} - -.x8 { - background-color: #f4b17a; color: white; + background: rgba(255, 255, 255, 0.06); + text-shadow: 0 0 10px white; + + transition: 0.1s; } -.x16{ - background-color: #f59575; - color: white; +/* animasi tile baru */ +.tile.new { + animation: pop 0.25s ease-out; +} +@keyframes pop { + 0% { transform: scale(0.2); opacity: 0; } + 100% { transform: scale(1); opacity: 1; } } -.x32{ - background-color: #f57c5f; - color: white; -} +/* Neon warna berdasarkan angka */ +.tile-2 { background: #00eaff55; box-shadow: 0 0 10px #00eaff; } +.tile-4 { background: #00ff9955; box-shadow: 0 0 10px #00ff99; } +.tile-8 { background: #ff00ff55; box-shadow: 0 0 10px #ff00ff; } +.tile-16 { background: #ff006655; box-shadow: 0 0 10px #ff0066; } +.tile-32 { background: #ffaa0055; box-shadow: 0 0 10px #ffaa00; } +.tile-64 { background: #ff000055; box-shadow: 0 0 10px #ff0000; } +.tile-128 { background: #5f00ff55; box-shadow: 0 0 10px #5f00ff; } +.tile-256 { background: #00ffea55; box-shadow: 0 0 10px #00ffea; } +.tile-512 { background: #ff00aa55; box-shadow: 0 0 10px #ff00aa; } +.tile-1024 { background: #00ffaa55; box-shadow: 0 0 10px #00ffaa; } +.tile-2048 { background: #ffd70066; box-shadow: 0 0 15px #ffd700; } -.x64{ - background-color: #f65d3b; - color: white; -} - -.x128{ - background-color: #edce71; - color: white; -} - -.x256{ - background-color: #edcc63; - color: white; -} - -.x512{ - background-color: #edc651; - color: white; -} - -.x1024{ - background-color: #eec744; - color: white; -} - -.x2048{ - background-color: #ecc230; - color: white; -} - -.x4096 { - background-color: #fe3d3d; - color: white; -} - -.x8192 { - background-color: #ff2020; - color: white; -} \ No newline at end of file diff --git a/2048.html b/2048.html index d68afa0..a150f2e 100644 --- a/2048.html +++ b/2048.html @@ -5,14 +5,19 @@ 2048 -

2048

+
+ + +
+

Score: 0

+ \ No newline at end of file diff --git a/2048.js b/2048.js new file mode 100644 index 0000000..9f53558 --- /dev/null +++ b/2048.js @@ -0,0 +1,320 @@ +/* 2048.js - Full version with sounds, vibration, stable grid rendering + Requirements: + - Place just BEFORE + - Put audio files in same folder (or adjust paths): + - bgmusic.mp3 + - pop.mp3 + - merge.wav +*/ + +let board = []; +let score = 0; + +// --- Audio setup --- +const audio = { + bg: new Audio("bgmusic.mp3"), + pop: new Audio("pop.mp3"), + merge: new Audio("merge.wav") +}; + +// lower default volumes +audio.bg.volume = 0.25; +audio.pop.volume = 0.9; +audio.merge.volume = 0.9; +audio.bg.loop = true; + +// try to play background music; may be blocked until user interaction +function tryPlayBg() { + audio.bg.play().catch(() => { + // autoplay blocked — will try again on first user keypress or click + 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: setup board and initial tiles --- +document.addEventListener("DOMContentLoaded", () => { + setupBoard(); + addNewTile(); + addNewTile(); + tryPlayBg(); + + // keyboard controls (also used to unlock audio) + document.addEventListener("keydown", handleKey); +}); + +// ---------------------------- +// SETUP BOARD (render tiles ONCE) +// ---------------------------- +function setupBoard() { + board = []; + score = 0; + updateScore(); + + const container = document.getElementById("board"); + if (!container) { + console.error("Board element not found (#board)."); + return; + } + + // empty container and create fixed 4x4 cells + 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"; // base class only + // ensure box sizing not influenced by text nodes + tile.style.boxSizing = "border-box"; + container.appendChild(tile); + } + } +} + +// ---------------------------- +// UPDATE UI FOR SINGLE TILE +// ---------------------------- +function updateTile(row, col, num) { + const tile = document.getElementById(`${row}-${col}`); + if (!tile) return; + + // reset classes to base + tile.className = "tile"; + + // force reflow to allow re-adding 'new' animation class reliably + void tile.offsetWidth; + + if (num > 0) { + tile.textContent = num; + // add tile class for color (expects classes like tile-2, tile-4, ...) + tile.classList.add("tile-" + num); + + // make numbers visually white-neon glow if desired: + // ensure text is centered by CSS; no inline style needed + } else { + tile.textContent = ""; + } +} + +// updates entire board DOM from board array +function refreshBoard() { + for (let r = 0; r < 4; r++) { + for (let c = 0; c < 4; c++) { + updateTile(r, c, board[r][c]); + } + } + updateScore(); +} + +// ---------------------------- +// SCORE UI +// ---------------------------- +function updateScore() { + const el = document.getElementById("score"); + if (el) el.textContent = score; +} + +// ---------------------------- +// ADD NEW TILE (value 2, play pop sound + 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"); + // play pop sound + playSound(audio.pop); + // remove 'new' class after animation completes to keep DOM tidy + tile.addEventListener("animationend", function handler() { + tile.classList.remove("new"); + tile.removeEventListener("animationend", handler); + }); + // update text/color + updateTile(spot.r, spot.c, 2); + } else { + // fallback + updateTile(spot.r, spot.c, 2); + } + return true; +} + +// helper to play sound safely +function playSound(soundObj) { + try { + soundObj.currentTime = 0; + soundObj.play().catch(() => { + // suppressed (autoplay or other) + }); + } catch (e) { /* ignore */ } +} + +// ---------------------------- +// INPUT HANDLING +// ---------------------------- +function handleKey(e) { + let moved = false; + if (e.key === "ArrowLeft") moved = moveLeft(); + else if (e.key === "ArrowRight") moved = moveRight(); + else if (e.key === "ArrowUp") moved = moveUp(); + else if (e.key === "ArrowDown") moved = moveDown(); + + if (moved) { + // after a successful move: add tile, refresh board, play bg or unlock + addNewTile(); + refreshBoard(); + } +} + +// ---------- MOVE HELPERS ---------- +function filterZero(row) { + return row.filter(n => n !== 0); +} + +function slide(row) { + // row is an array of length 4 + 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; + // play merge sound and vibrate + playSound(audio.merge); + if (navigator.vibrate) navigator.vibrate(30); + + score += row[i]; + row[i + 1] = 0; + mergedThisMove = true; + } + } + + row = filterZero(row); + while (row.length < 4) row.push(0); + return { row, merged: mergedThisMove }; +} + +// Compare arrays helper +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; +} + +function updateAfterMove() { + // update all tiles now (this keeps sizes stable) + refreshBoard(); + // update score DOM + updateScore(); + // small debounce is not required—moves are sequential +} + +// ---------------------------- +// RESTART & HOME +// ---------------------------- +function restartGame() { + setupBoard(); + addNewTile(); + addNewTile(); + refreshBoard(); +} + +function goHome() { + // stops music to prevent continuing on homepage + try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} + window.location.href = "Homepage.html"; +} + +// ---------------------------- +// OPTIONAL: touch swipe for mobile +// ---------------------------- +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) moveRight() && addNewTile() && refreshBoard(); + else moveLeft() && addNewTile() && refreshBoard(); + } else if (Math.abs(dy) > 30) { + if (dy > 0) moveDown() && addNewTile() && refreshBoard(); + else moveUp() && addNewTile() && refreshBoard(); + } +}, { passive: true }); + +/* End of file */