diff --git a/2048.css b/2048.css index d425735..349a044 100644 --- a/2048.css +++ b/2048.css @@ -1,6 +1,10 @@ -/* ====================== - GLOBAL -====================== */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + body { background: radial-gradient(circle at center, #0a0a0a, #000); background-size: 300% 300%; @@ -8,93 +12,245 @@ body { font-family: 'Poppins', sans-serif; color: white; text-align: center; - padding-top: 20px; + min-height: 100vh; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 10px; + overflow: hidden; } -/* Background animasi lembut */ @keyframes bgMove { 0% { background-position: 0% 30%; } 50% { background-position: 50% 70%; } 100% { background-position: 100% 30%; } } +/* ====================== + GAME CONTAINER +====================== */ +.game-container { + width: 100%; + max-width: 480px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + transform: scale(0.95); +} + +/* ====================== + HEADER - All Centered Vertically +====================== */ +.game-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + width: 100%; +} + h1 { - font-size: 40px; - font-weight: bold; - text-shadow: 0 0 20px #00eaff, 0 0 40px #0099ff; + font-size: clamp(42px, 7vw, 68px); + font-weight: 800; + text-shadow: 0 0 25px #00eaff, 0 0 45px #0099ff; + letter-spacing: clamp(4px, 1vw, 8px); + margin: 0 0 clamp(10px, 1.5vh, 16px) 0; + line-height: 1; } -button { - padding: 12px 22px; - margin: 8px; - background: #111; +/* Score Container - Centered below title */ +.score-container { + display: flex; + gap: clamp(10px, 2vw, 14px); + justify-content: center; + margin-bottom: clamp(12px, 2vh, 16px); +} + +.score-box { + background: rgba(30, 0, 50, 0.85); + backdrop-filter: blur(15px); + border: 2px solid rgba(0, 217, 255, 0.45); + border-radius: 12px; + padding: clamp(10px, 1.5vh, 12px) clamp(20px, 3vw, 26px); + min-width: clamp(85px, 12vw, 105px); + box-shadow: + 0 5px 20px rgba(0, 0, 0, 0.4), + 0 0 20px rgba(0, 217, 255, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.score-box:hover { + border-color: rgba(0, 217, 255, 0.7); + box-shadow: + 0 5px 20px rgba(0, 0, 0, 0.4), + 0 0 30px rgba(0, 217, 255, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); +} + +.score-label { + font-size: clamp(10px, 1.3vw, 12px); + text-transform: uppercase; + color: rgba(0, 234, 255, 0.85); + letter-spacing: 1.2px; + margin-bottom: 5px; + font-weight: 700; +} + +.score-value { + font-size: clamp(22px, 3vw, 28px); + font-weight: 800; color: white; - border: 2px solid #00eaff; - border-radius: 10px; + text-shadow: 0 0 12px rgba(0, 234, 255, 0.9); + line-height: 1; +} + +/* ====================== + TOP CONTROLS - Icon Buttons (Top Right) +====================== */ +.top-controls { + position: fixed; + top: clamp(10px, 2vh, 20px); + right: clamp(10px, 2vw, 20px); + display: flex; + gap: clamp(8px, 1.5vw, 12px); + z-index: 100; +} + +.icon-btn { + width: clamp(36px, 6vw, 48px); + height: clamp(36px, 6vw, 48px); + padding: 0; + background: rgba(30, 0, 50, 0.85); + backdrop-filter: blur(15px); + border: 2px solid rgba(0, 217, 255, 0.45); + border-radius: 12px; cursor: pointer; - transition: 0.25s; - font-weight: bold; -} -button:hover { - box-shadow: 0 0 15px #00eaff; - transform: scale(1.05); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 0 4px 18px rgba(0, 0, 0, 0.35), + 0 0 20px rgba(0, 217, 255, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); } -#score { - color: #00eaff; - text-shadow: 0 0 10px #00eaff; - font-weight: bold; - font-size: 20px; +.icon-btn svg { + width: clamp(18px, 3vw, 24px); + height: clamp(18px, 3vw, 24px); + color: rgba(0, 234, 255, 0.9); + transition: all 0.3s ease; } +.icon-btn:hover { + background: rgba(0, 234, 255, 0.15); + border-color: rgba(0, 234, 255, 0.8); + box-shadow: + 0 6px 25px rgba(0, 0, 0, 0.45), + 0 0 35px rgba(0, 234, 255, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + transform: translateY(-2px); +} + +.icon-btn:hover svg { + color: rgba(0, 234, 255, 1); + transform: scale(1.1); +} + +.btn-restart-icon:hover svg { + transform: scale(1.1) rotate(180deg); +} + +.icon-btn:active { + transform: translateY(0); + box-shadow: + 0 2px 12px rgba(0, 0, 0, 0.35), + 0 0 20px rgba(0, 234, 255, 0.3), + inset 0 2px 6px rgba(0, 0, 0, 0.25); +} + +/* Tutorial Button Specific */ +.btn-tutorial { + background: rgba(50, 0, 70, 0.85); + border-color: rgba(200, 100, 255, 0.45); +} + +.btn-tutorial svg { + color: rgba(200, 100, 255, 0.9); +} + +.btn-tutorial:hover { + background: rgba(200, 100, 255, 0.15); + border-color: rgba(200, 100, 255, 0.8); + box-shadow: + 0 6px 25px rgba(0, 0, 0, 0.45), + 0 0 35px rgba(200, 100, 255, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); +} + +.btn-tutorial:hover svg { + color: rgba(200, 100, 255, 1); +} /* ========================== - BOARD — MIRIP BORDER LOGIN + BOARD ========================== */ - #board { - width: 460px; - height: 460px; - margin: 35px auto; - padding: 20px; - + width: 100%; + max-width: min(480px, 85vmin); + aspect-ratio: 1; + margin: 0 auto; + padding: clamp(10px, 2vmin, 18px); display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); - 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 */ + gap: clamp(6px, 1.2vmin, 12px); + background: rgba(30, 0, 50, 0.65); + backdrop-filter: blur(12px); + border: 2px solid rgba(0, 217, 255, 0.35); 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; + 0 25px 70px rgba(0, 0, 0, 0.6), + 0 0 45px rgba(0, 217, 255, 0.35), + inset 0 0 35px rgba(0, 217, 255, 0.12); + animation: glowBorderBoard 3.5s ease-in-out infinite; + position: relative; + z-index: 2; } -/* 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); + 0 25px 70px rgba(0, 0, 0, 0.6), + 0 0 45px rgba(0, 217, 255, 0.35), + inset 0 0 35px rgba(0, 217, 255, 0.12); } 50% { - border-color: rgba(255, 0, 255, 0.5); + border-color: rgba(255, 0, 255, 0.55); 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); + 0 25px 70px rgba(0, 0, 0, 0.6), + 0 0 55px rgba(255, 0, 255, 0.45), + inset 0 0 45px rgba(255, 0, 255, 0.18); } } +#board::after { + content: ""; + position: absolute; + bottom: -40px; + left: 50%; + transform: translateX(-50%); + width: 360px; + height: 90px; + background: radial-gradient(ellipse at center, rgba(0,234,255,0.14), transparent 65%); + filter: blur(35px); + z-index: -1; +} /* ====================== TILE @@ -102,171 +258,777 @@ button:hover { .tile { width: 100%; height: 100%; - border-radius: 14px; + border-radius: clamp(8px, 1.8vmin, 14px); display: flex; align-items: center; justify-content: center; - - font-size: 32px; - font-weight: bold; - + font-size: clamp(20px, 3.5vmin, 32px); + font-weight: 800; color: white; - background: rgba(255, 255, 255, 0.06); - text-shadow: 0 0 10px white; - - transition: 0.1s; + background: rgba(255, 255, 255, 0.07); + text-shadow: 0 0 12px rgba(255, 255, 255, 0.9); + transition: transform 0.15s cubic-bezier(.2,.85,.2,1), opacity 0.13s linear, box-shadow 0.13s; + will-change: transform, opacity, box-shadow; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; } -/* animasi tile baru */ .tile.new { - animation: pop 0.25s ease-out; + animation: pop 0.24s cubic-bezier(.2,.9,.2,1); } + @keyframes pop { - 0% { transform: scale(0.2); opacity: 0; } + 0% { transform: scale(0.15); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } -/* 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; } +.tile.merge { + animation: mergePop 0.26s ease-out; + z-index: 3; +} -/* ======= ENHANCEMENTS: Animations, Particles, Glows ======= */ +@keyframes mergePop { + 0% { transform: scale(1); box-shadow: 0 0 8px rgba(255,255,255,0.12); } + 50% { transform: scale(1.2); box-shadow: 0 0 35px rgba(255,255,255,0.4); } + 100% { transform: scale(1); box-shadow: 0 0 8px rgba(255,255,255,0.12); } +} -/* ensure board stacking context for absolute animations */ -#board { position: relative; z-index: 2; } +#board.shake { + animation: shakeBoard 0.4s; +} +@keyframes shakeBoard { + 0%,100% { transform: translateX(0); } + 25% { transform: translateX(-10px); } + 75% { transform: translateX(10px); } +} + +/* Tile Colors */ +.tile-2 { background: #00eaff55; box-shadow: 0 0 12px #00eaff; } +.tile-4 { background: #00ff9955; box-shadow: 0 0 12px #00ff99; } +.tile-8 { background: #ff00ff55; box-shadow: 0 0 12px #ff00ff; } +.tile-16 { background: #ff006655; box-shadow: 0 0 12px #ff0066; } +.tile-32 { background: #ffaa0055; box-shadow: 0 0 12px #ffaa00; } +.tile-64 { background: #ff000055; box-shadow: 0 0 12px #ff0000; } +.tile-128 { background: #5f00ff55; box-shadow: 0 0 12px #5f00ff; } +.tile-256 { background: #00ffea55; box-shadow: 0 0 12px #00ffea; } +.tile-512 { background: #ff00aa55; box-shadow: 0 0 12px #ff00aa; } +.tile-1024 { background: #00ffaa55; box-shadow: 0 0 12px #00ffaa; } +.tile-2048 { + background: #ffd70066; + box-shadow: 0 0 18px #ffd700; + animation: goldShimmer 2.6s infinite; +} + +@keyframes goldShimmer { + 0% { box-shadow: 0 0 12px #ffd70066; transform: translateZ(0); } + 50% { box-shadow: 0 0 30px #ffd700aa; transform: translateY(-2px); } + 100% { box-shadow: 0 0 12px #ffd70066; transform: translateZ(0); } +} + +/* Enhanced Merge Animation */ +.tile.merge { + animation: mergeBounce 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + z-index: 10; +} + +@keyframes mergeBounce { + 0% { + transform: scale(1); + filter: brightness(1); + } + 40% { + transform: scale(1.25) rotate(5deg); + filter: brightness(1.4) drop-shadow(0 0 25px currentColor); + } + 60% { + transform: scale(0.95) rotate(-3deg); + filter: brightness(1.2); + } + 80% { + transform: scale(1.08); + filter: brightness(1.1); + } + 100% { + transform: scale(1) rotate(0deg); + filter: brightness(1); + } +} + +/* Particle Effect */ +.merge-particle { + position: fixed; + width: 10px; + height: 10px; + border-radius: 50%; + pointer-events: none; + z-index: 9998; + box-shadow: + 0 0 10px currentColor, + 0 0 20px currentColor, + inset 0 0 5px rgba(255, 255, 255, 0.5); + filter: blur(0.5px); +} + +/* Score Popup Animation */ +.score-popup { + animation: scoreFloat 1s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +@keyframes scoreFloat { + 0% { + transform: translate(-50%, -50%) scale(0.5); + opacity: 0; + } + 30% { + transform: translate(-50%, -70px) scale(1.2); + opacity: 1; + } + 100% { + transform: translate(-50%, -120px) scale(1); + opacity: 0; + } +} + +/* Enhanced Tile Glow on Merge */ +.tile.merge { + box-shadow: + 0 0 30px currentColor, + 0 0 50px currentColor, + inset 0 0 20px rgba(255, 255, 255, 0.3) !important; +} + +/* Smoother New Tile Animation */ +.tile.new { + animation: popIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +@keyframes popIn { + 0% { + transform: scale(0) rotate(-180deg); + opacity: 0; + filter: blur(4px); + } + 70% { + transform: scale(1.15) rotate(10deg); + filter: blur(0); + } + 100% { + transform: scale(1) rotate(0deg); + opacity: 1; + filter: blur(0); + } +} + +/* Tile Hover Effect - Make it more interactive */ +.tile:not(:empty):hover { + transform: scale(1.05); + transition: transform 0.2s ease; + filter: brightness(1.2); +} + +/* Add ripple effect on board when moving */ +#board.moving { + animation: boardPulse 0.3s ease-out; +} + +@keyframes boardPulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.01); + } +} + +/* ========================== + TUTORIAL MODAL +========================== */ +.tutorial-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.88); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + z-index: 998; + animation: fadeIn 0.3s ease-out; +} + +.tutorial-modal { + background: rgba(20, 0, 40, 0.96); + backdrop-filter: blur(25px); + border: 3px solid rgba(200, 100, 255, 0.5); + border-radius: 24px; + padding: 45px 50px; + max-width: 500px; + width: 90%; + text-align: center; + box-shadow: + 0 35px 90px rgba(0, 0, 0, 0.75), + 0 0 60px rgba(200, 100, 255, 0.4), + inset 0 0 50px rgba(200, 100, 255, 0.1); + animation: modalSlideUp 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); + position: relative; +} + +.modal-close { + position: absolute; + top: 15px; + right: 15px; + width: 36px; + height: 36px; + padding: 0; + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-close svg { + width: 20px; + height: 20px; + color: rgba(255, 255, 255, 0.7); +} + +.modal-close:hover { + background: rgba(255, 50, 50, 0.2); + border-color: rgba(255, 50, 50, 0.6); + transform: rotate(90deg); +} + +.modal-close:hover svg { + color: #ff5050; +} + +.tutorial-title { + font-size: 32px; + font-weight: 800; + color: #c864ff; + text-shadow: 0 0 25px rgba(200, 100, 255, 0.8); + margin-bottom: 30px; + letter-spacing: 2px; +} + +.tutorial-content { + display: flex; + flex-direction: column; + gap: 30px; +} + +.tutorial-section h3 { + font-size: 18px; + color: rgba(200, 100, 255, 0.9); + margin-bottom: 15px; + font-weight: 700; +} + +/* Show/Hide based on device */ +.mobile-controls { + display: none; +} + +@media (max-width: 768px) { + .pc-controls { + display: none !important; + } + + .mobile-controls { + display: block; + } +} + +/* Keys Display */ +.keys-container { + display: flex; + align-items: center; + justify-content: center; + gap: clamp(20px, 5vw, 40px); + flex-wrap: nowrap; +} + +.keys-group { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.keys-grid-wasd, +.keys-grid-arrow { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 8px; + justify-items: center; + align-items: center; +} + +.keys-separator { + font-size: clamp(14px, 2.2vw, 18px); + color: rgba(255, 255, 255, 0.5); + font-weight: 700; + padding: 0 clamp(10px, 2vw, 15px); +} + +.key-box { + width: clamp(42px, 6.5vw, 52px); + height: clamp(42px, 6.5vw, 52px); + background: rgba(0, 234, 255, 0.15); + border: 2px solid rgba(0, 234, 255, 0.5); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: clamp(17px, 2.8vw, 22px); + font-weight: 700; + color: #00eaff; + text-shadow: 0 0 10px rgba(0, 234, 255, 0.8); + box-shadow: + 0 4px 15px rgba(0, 0, 0, 0.3), + 0 0 20px rgba(0, 234, 255, 0.2), + inset 0 -2px 0 rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.key-empty { + width: clamp(42px, 6.5vw, 52px); + height: clamp(42px, 6.5vw, 52px); + visibility: hidden; +} + +.keys-label { + font-size: clamp(12px, 1.8vw, 14px); + color: rgba(255, 255, 255, 0.6); + margin-top: 5px; + font-weight: 600; + text-align: center; +} + +/* Swipe Demo */ +.swipe-demo { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.swipe-icon { + font-size: 48px; + animation: swipeAnimation 2s ease-in-out infinite; +} + +@keyframes swipeAnimation { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-15px); } + 50% { transform: translateX(0); } + 75% { transform: translateX(15px); } +} + +.swipe-demo p { + font-size: 14px; + color: rgba(255, 255, 255, 0.7); +} + +/* Objective Text */ +.objective-text { + font-size: 15px; + color: rgba(255, 255, 255, 0.85); + line-height: 1.6; +} + +.objective-text strong { + color: #ffd700; + text-shadow: 0 0 10px rgba(255, 215, 0, 0.6); + font-weight: 800; +} + +/* ========================== + GAME OVER MODAL - WITH ICON BUTTONS +========================== */ +/* ========================== + GAME OVER MODAL - REVISED VERSION + Copy bagian ini dan replace yang lama di 2048.css +========================== */ + +.game-over-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.93); + backdrop-filter: blur(15px); + display: flex; + align-items: center; + justify-content: center; + z-index: 999; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.game-over-modal { + background: linear-gradient(145deg, rgba(15, 0, 35, 0.98), rgba(25, 0, 45, 0.98)); + backdrop-filter: blur(30px); + border: 3px solid rgba(255, 50, 100, 0.5); + border-radius: 32px; + padding: 55px 50px 45px; + max-width: 460px; + width: 90%; + text-align: center; + box-shadow: + 0 50px 120px rgba(0, 0, 0, 0.85), + 0 0 100px rgba(255, 50, 100, 0.4), + inset 0 2px 80px rgba(255, 50, 100, 0.06); + animation: modalSlideUp 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); + position: relative; +} + +@keyframes modalSlideUp { + from { + opacity: 0; + transform: translateY(60px) scale(0.85); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Close Button (X) - Top Right */ +.game-over-close { + position: absolute; + top: 18px; + right: 18px; + width: 38px; + height: 38px; + padding: 0; + background: rgba(255, 255, 255, 0.06); + border: 2px solid rgba(255, 255, 255, 0.15); + border-radius: 10px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.game-over-close svg { + width: 20px; + height: 20px; + color: rgba(255, 255, 255, 0.6); + transition: all 0.3s ease; +} + +.game-over-close:hover { + background: rgba(255, 80, 80, 0.2); + border-color: rgba(255, 80, 80, 0.6); + transform: rotate(90deg) scale(1.05); +} + +.game-over-close:hover svg { + color: #ff6666; +} + +.game-over-close:active { + transform: rotate(90deg) scale(0.95); +} + +/* Title - Gradient Pink/Red */ +.game-over-title { + font-size: clamp(32px, 5vw, 42px); + font-weight: 900; + background: linear-gradient(135deg, #ff3366 0%, #ff6699 50%, #ff99cc 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 35px rgba(255, 51, 102, 0.5)); + margin-bottom: 12px; + letter-spacing: 4px; + text-transform: uppercase; + line-height: 1.2; +} + +/* Subtitle */ +.game-over-subtitle { + font-size: 14px; + color: rgba(255, 255, 255, 0.5); + letter-spacing: 2px; + margin-bottom: 35px; + font-weight: 600; + text-transform: uppercase; +} + +/* Score Section - PURPLE/VIOLET gradient */ +.game-over-score { + margin: 0 0 38px; +} + +.game-over-score-label { + font-size: 11px; + text-transform: uppercase; + color: rgba(200, 100, 255, 0.7); + letter-spacing: 3px; + margin-bottom: 14px; + font-weight: 700; +} + +.game-over-score-value { + font-size: clamp(52px, 8vw, 68px); + font-weight: 900; + background: linear-gradient(135deg, #c864ff 0%, #8b5cf6 50%, #a855f7 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 30px rgba(200, 100, 255, 0.6)); + margin-bottom: 10px; + line-height: 1; + letter-spacing: -2px; +} + +/* New High Score Badge - GOLD */ +.new-high-score { + display: inline-block; + background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 165, 0, 0.2)); + border: 2.5px solid rgba(255, 215, 0, 0.7); + color: #ffd700; + padding: 12px 26px; + border-radius: 50px; + font-size: 13px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 2px; + margin-top: 18px; + box-shadow: + 0 0 35px rgba(255, 215, 0, 0.35), + inset 0 0 30px rgba(255, 215, 0, 0.1); + animation: newHighScorePulse 2s ease-in-out infinite; +} + +@keyframes newHighScorePulse { + 0%, 100% { + box-shadow: + 0 0 30px rgba(255, 215, 0, 0.4), + inset 0 0 30px rgba(255, 215, 0, 0.1); + border-color: rgba(255, 215, 0, 0.7); + transform: scale(1); + } + 50% { + box-shadow: + 0 0 50px rgba(255, 215, 0, 0.7), + inset 0 0 40px rgba(255, 215, 0, 0.2); + border-color: rgba(255, 215, 0, 1); + transform: scale(1.02); + } +} + +/* Best Score Display - ORANGE gradient */ +.best-score-display { + margin-top: 28px; + padding-top: 28px; + border-top: 2px solid rgba(255, 140, 0, 0.25); +} + +.best-score-label { + font-size: 11px; + text-transform: uppercase; + color: rgba(255, 160, 50, 0.7); + letter-spacing: 2.5px; + margin-bottom: 10px; + font-weight: 700; +} + +.best-score-value { + font-size: clamp(34px, 5vw, 42px); + font-weight: 900; + background: linear-gradient(135deg, #ff8c00 0%, #ffa500 50%, #ffb347 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 20px rgba(255, 140, 0, 0.5)); + line-height: 1; + letter-spacing: -1px; +} + +/* =========================== + ICON BUTTONS - Enhanced Color +=========================== */ +.game-over-buttons { + display: flex; + gap: 16px; + justify-content: center; + margin-top: 42px; +} + +/* Icon Button Base Style */ +.btn-game-icon { + width: clamp(70px, 10vw, 80px); + height: clamp(70px, 10vw, 80px); + padding: 0; + background: rgba(255, 255, 255, 0.06); + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(10px); + box-shadow: + 0 8px 30px rgba(0, 0, 0, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; +} + +.btn-game-icon svg { + width: clamp(32px, 5vw, 38px); + height: clamp(32px, 5vw, 38px); + transition: all 0.3s ease; + position: relative; + z-index: 2; +} + +/* Shine Effect */ +.btn-game-icon::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); + transition: left 0.5s ease; +} + +.btn-game-icon:hover::before { + left: 100%; +} + +.btn-game-icon:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.4); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.5), + 0 0 40px rgba(255, 255, 255, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + transform: translateY(-4px); +} + +.btn-game-icon:active { + transform: translateY(0); + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.4), + inset 0 2px 8px rgba(0, 0, 0, 0.3); +} + +/* Restart Button - EMERALD GREEN */ +.btn-restart-game { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.2)); + border-color: rgba(16, 185, 129, 0.6); + box-shadow: + 0 8px 30px rgba(0, 0, 0, 0.4), + 0 0 30px rgba(16, 185, 129, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.btn-restart-game svg { + color: #10b981; + filter: drop-shadow(0 0 12px rgba(16, 185, 129, 0.5)); +} + +.btn-restart-game:hover { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3)); + border-color: rgba(16, 185, 129, 0.9); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.5), + 0 0 50px rgba(16, 185, 129, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.btn-restart-game:hover svg { + transform: rotate(180deg); + color: #34d399; +} + +/* Home Button - SKY BLUE */ +.btn-home-game { + background: linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(2, 132, 199, 0.2)); + border-color: rgba(14, 165, 233, 0.6); + box-shadow: + 0 8px 30px rgba(0, 0, 0, 0.4), + 0 0 30px rgba(14, 165, 233, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.btn-home-game svg { + color: #0ea5e9; + filter: drop-shadow(0 0 12px rgba(14, 165, 233, 0.5)); +} + +.btn-home-game:hover { + background: linear-gradient(135deg, rgba(14, 165, 233, 0.3), rgba(2, 132, 199, 0.3)); + border-color: rgba(14, 165, 233, 0.9); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.5), + 0 0 50px rgba(14, 165, 233, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.btn-home-game:hover svg { + transform: scale(1.1); + color: #38bdf8; +} + +/* Responsive Design */ +@media (max-width: 480px) { + .game-over-modal { + padding: 45px 35px 38px; + } + + .game-over-buttons { + gap: 12px; + } + + .btn-game-icon { + width: clamp(65px, 15vw, 75px); + height: clamp(65px, 15vw, 75px); + } + + .btn-game-icon svg { + width: clamp(28px, 6vw, 34px); + height: clamp(28px, 6vw, 34px); + } +} +/* ========================== + BACKGROUND EFFECTS +========================== */ .particles { position: fixed; inset: 0; pointer-events: none; z-index: 0; - background: - radial-gradient(circle at 20% 30%, rgba(0,234,255,0.18), transparent 45%), - radial-gradient(circle at 80% 70%, rgba(255,0,90,0.18), transparent 45%), - radial-gradient(circle at 50% 50%, rgba(140,0,255,0.14), transparent 55%), - radial-gradient(circle at 10% 85%, rgba(0,255,200,0.12), transparent 55%), - radial-gradient(circle at 90% 15%, rgba(255,0,255,0.15), transparent 45%); - - filter: blur(55px) brightness(120%) saturate(130%); - animation: particlesFloat 18s ease-in-out infinite alternate; + radial-gradient(circle at 20% 30%, rgba(0,234,255,0.2), transparent 48%), + radial-gradient(circle at 80% 70%, rgba(255,0,90,0.2), transparent 48%), + radial-gradient(circle at 50% 50%, rgba(140,0,255,0.16), transparent 58%), + radial-gradient(circle at 10% 85%, rgba(0,255,200,0.14), transparent 58%), + radial-gradient(circle at 90% 15%, rgba(255,0,255,0.17), transparent 48%); + filter: blur(60px) brightness(125%) saturate(135%); + animation: particlesFloat 20s ease-in-out infinite alternate; } @keyframes particlesFloat { 0% { transform: translateY(0px) translateX(0px); } - 50% { transform: translateY(-25px) translateX(10px); } - 100% { transform: translateY(-40px) translateX(-15px); } -} - - -.cursor-light { - position: absolute; - width: 240px; - height: 240px; - background: radial-gradient(circle, rgba(0,255,255,0.25), transparent 70%); - border-radius: 50%; - pointer-events: none; - transform: translate(-50%, -50%); - filter: blur(40px); - mix-blend-mode: screen; - opacity: 0.6; -} - - -/* gentle vertical float */ -@keyframes floatBg { 0% { transform: translateY(0);} 100% { transform: translateY(-14px);} } - -/* Tile move / slide illusion: tiles appear from direction */ -.tile { - transition: transform 0.14s cubic-bezier(.2,.8,.2,1), opacity 0.12s linear, box-shadow 0.12s; - will-change: transform, opacity, box-shadow; -} - -/* 'new' pop kept but slightly tuned */ -.tile.new { - animation: pop 0.22s cubic-bezier(.2,.9,.2,1); -} - -/* merge pop */ -.tile.merge { - animation: mergePop 0.24s ease-out; - z-index: 3; -} -@keyframes mergePop { - 0% { transform: scale(1); box-shadow: 0 0 6px rgba(255,255,255,0.1); } - 50% { transform: scale(1.18); box-shadow: 0 0 30px rgba(255,255,255,0.35); } - 100% { transform: scale(1); box-shadow: 0 0 6px rgba(255,255,255,0.1); } -} - -/* board shake when invalid move */ -#board.shake { - animation: shakeBoard 0.36s; -} -@keyframes shakeBoard { - 0%,100% { transform: translateX(0); } - 25% { transform: translateX(-8px); } - 75% { transform: translateX(8px); } -} - -/* ambient glow under board (keeps aesthetics) */ -#board::after { - content: ""; - position: absolute; - bottom: -36px; - left: 50%; - transform: translateX(-50%); - width: 340px; - height: 84px; - background: radial-gradient(ellipse at center, rgba(0,234,255,0.12), transparent 60%); - filter: blur(30px); - z-index: 0; -} - -/* special 2048 tile sparkle (adds shimmer) */ -.tile-2048 { - animation: goldShimmer 2.4s infinite; -} -@keyframes goldShimmer { - 0% { box-shadow: 0 0 10px #ffd70066; transform: translateZ(0); } - 50% { box-shadow: 0 0 26px #ffd700aa; transform: translateY(-2px); } - 100% { box-shadow: 0 0 10px #ffd70066; transform: translateZ(0); } -} - -/* small particle bits that appear for merge */ -.merge-particle { - position: absolute; - width: 8px; - height: 8px; - border-radius: 50%; - pointer-events: none; - opacity: 0.95; - will-change: transform, opacity; - filter: blur(1px) drop-shadow(0 0 6px rgba(255,255,255,0.08)); -} - -/* ensure base tile readability on animation */ -.tile { backface-visibility: hidden; -webkit-backface-visibility: hidden; } - -/* mobile touch hint (optional) */ -.touch-hint { - position: fixed; - right: 18px; - bottom: 18px; - background: rgba(0,0,0,0.45); - padding: 8px 10px; - border-radius: 10px; - font-size: 12px; - z-index: 4; - color: #cfefff; + 50% { transform: translateY(-30px) translateX(12px); } + 100% { transform: translateY(-45px) translateX(-18px); } } .starfield { @@ -281,14 +1043,65 @@ button:hover { position: absolute; width: 4px; height: 4px; - background: rgba(0,255,255,0.8); + background: rgba(0,255,255,0.85); border-radius: 50%; filter: blur(2px); animation: starMove linear infinite; } -/* bintang bergerak random */ @keyframes starMove { - 0% { transform: translateY(0); opacity: 0.8; } - 100% { transform: translateY(-700px); opacity: 0; } + 0% { transform: translateY(0); opacity: 0.85; } + 100% { transform: translateY(-750px); opacity: 0; } } + +.cursor-light { + position: absolute; + width: 250px; + height: 250px; + background: radial-gradient(circle, rgba(0,255,255,0.28), transparent 72%); + border-radius: 50%; + pointer-events: none; + transform: translate(-50%, -50%); + filter: blur(45px); + mix-blend-mode: screen; + opacity: 0.65; +} + +.merge-particle { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + pointer-events: none; + opacity: 0.95; + will-change: transform, opacity; + filter: blur(1px) drop-shadow(0 0 8px rgba(255,255,255,0.1)); +} + +/* ========================== + RESPONSIVE DESIGN +========================== */ + +/* Touch hint for mobile only */ +.touch-hint { + display: none; +} + +@media (max-width: 768px) { + .touch-hint { + display: block; + position: fixed; + right: clamp(10px, 2vw, 16px); + bottom: clamp(10px, 2vh, 16px); + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(12px); + padding: clamp(7px, 1.5vw, 9px) clamp(11px, 2vw, 14px); + border-radius: 10px; + font-size: clamp(10px, 1.5vw, 11px); + z-index: 4; + color: #cfefff; + border: 1px solid rgba(0, 217, 255, 0.35); + box-shadow: 0 5px 18px rgba(0, 0, 0, 0.35); + font-weight: 500; + } +} \ No newline at end of file diff --git a/2048.html b/2048.html index c5c2f07..7a97490 100644 --- a/2048.html +++ b/2048.html @@ -1,26 +1,172 @@ - - - - - 2048 - - - - - -
-
-

2048

-
- - + + + + + 2048 + + + + + + + + + + + + +
+ + +
+ + +
+ +
+

2048

+
+
+
SCORE
+
0
+
+
+
HIGH SCORE
+
0
+
+
-
-

Score: 0

-
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/2048.js b/2048.js index fce07f0..71aa00e 100644 --- a/2048.js +++ b/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(); -} - -function goHome() { - try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} - window.location.href = "Homepage.html"; +/* 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; + } + } + + 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; - - 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); - } +function triggerMergeEffect(row, col) { + const tile = document.getElementById(`${row}-${col}`); + if (!tile) return; + + // Add merge class for CSS animation + tile.classList.add('merge'); + setTimeout(() => tile.classList.remove('merge'), 300); + + // Create particle burst effect + createParticleBurst(tile); + + // 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 + ------------------------ */ \ No newline at end of file