diff --git a/2048.css b/2048.css index 8ade4f5..c9d16a4 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,98 +12,414 @@ 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%; } } -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; +/* ====================== + GAME CONTAINER +====================== */ +.game-container { + width: 100%; + max-width: 480px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + transform: scale(0.95); } /* ====================== - BOARD + HEADER - All Centered Vertically ====================== */ +.game-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + width: 100%; +} +h1 { + 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; +} + +/* 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; + 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; +} + +.sound-controls { + position: fixed; + top: clamp(10px, 2vh, 20px); + left: clamp(10px, 2vw, 20px); + display: flex; + gap: clamp(8px, 1.5vw, 12px); + z-index: 100; +} + +/* Sound Button Styling */ +.btn-sound { + position: relative; + 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: 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); +} + +.btn-sound svg { + width: clamp(18px, 3vw, 24px); + height: clamp(18px, 3vw, 24px); + color: rgba(0, 234, 255, 0.9); + transition: all 0.3s ease; + position: absolute; +} + +/* BG Music Button - Purple */ +#btn-sound-bg { + background: rgba(50, 0, 70, 0.85); + border-color: rgba(200, 100, 255, 0.45); +} + +#btn-sound-bg svg { + color: rgba(200, 100, 255, 0.9); +} + +#btn-sound-bg: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-sound-bg:hover svg { + color: rgba(200, 100, 255, 1); +} + +/* Pop SFX Button - Cyan */ +#btn-sound-pop { + background: rgba(0, 40, 50, 0.85); + border-color: rgba(0, 234, 255, 0.45); +} + +#btn-sound-pop svg { + color: rgba(0, 234, 255, 0.9); +} + +#btn-sound-pop: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); +} + +#btn-sound-pop:hover svg { + color: rgba(0, 234, 255, 1); +} + +/* Merge SFX Button - Orange/Yellow */ +#btn-sound-merge { + background: rgba(60, 30, 0, 0.85); + border-color: rgba(255, 170, 0, 0.45); +} + +#btn-sound-merge svg { + color: rgba(255, 170, 0, 0.9); +} + +#btn-sound-merge:hover { + background: rgba(255, 170, 0, 0.15); + border-color: rgba(255, 170, 0, 0.8); + box-shadow: + 0 6px 25px rgba(0, 0, 0, 0.45), + 0 0 35px rgba(255, 170, 0, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); +} + +#btn-sound-merge:hover svg { + color: rgba(255, 170, 0, 1); +} + +/* Hover & Active States */ +.btn-sound:hover { + transform: translateY(-2px); +} + +.btn-sound: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); +} + +/* Muted State - Red with X */ +.btn-sound.muted { + background: rgba(60, 0, 10, 0.85) !important; + border-color: rgba(255, 50, 50, 0.6) !important; + box-shadow: + 0 4px 18px rgba(0, 0, 0, 0.35), + 0 0 20px rgba(255, 50, 50, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; +} + +.btn-sound.muted svg.sound-icon { + display: none !important; +} + +.btn-sound.muted svg.mute-icon { + display: block !important; + color: rgba(255, 80, 80, 0.9) !important; +} + +.btn-sound.muted:hover { + background: rgba(255, 50, 50, 0.2) !important; + border-color: rgba(255, 80, 80, 0.8) !important; + box-shadow: + 0 6px 25px rgba(0, 0, 0, 0.45), + 0 0 35px rgba(255, 50, 50, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.15) !important; +} + +.btn-sound.muted:hover svg.mute-icon { + color: rgba(255, 100, 100, 1) !important; +} + +/* Icon Transitions */ +.btn-sound svg.sound-icon, +.btn-sound svg.mute-icon { + transition: all 0.3s ease; +} + +.btn-sound:hover svg { + transform: scale(1.1); +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .sound-controls { + flex-direction: column; + gap: clamp(6px, 1.2vw, 10px); + } +} +.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: 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); +} + +.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 @@ -107,171 +427,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 { @@ -286,14 +1212,302 @@ 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; + } +} + +/* ========================== + ADVANCED SOUND CONTROL +========================== */ +.sound-control-container { + position: fixed; + top: clamp(10px, 2vh, 20px); + left: clamp(10px, 2vw, 20px); + z-index: 100; +} + +/* Main Sound Button */ +.btn-sound-main { + width: clamp(40px, 6.5vw, 52px); + height: clamp(40px, 6.5vw, 52px); + padding: 0; + background: rgba(30, 0, 50, 0.9); + backdrop-filter: blur(15px); + border: 2px solid rgba(0, 217, 255, 0.5); + border-radius: 14px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 0 5px 20px rgba(0, 0, 0, 0.4), + 0 0 25px rgba(0, 217, 255, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + position: relative; +} + +.btn-sound-main svg { + width: clamp(20px, 3.5vw, 26px); + height: clamp(20px, 3.5vw, 26px); + color: rgba(0, 234, 255, 0.95); + transition: all 0.3s ease; + position: absolute; +} + +.btn-sound-main:hover { + background: rgba(0, 234, 255, 0.18); + border-color: rgba(0, 234, 255, 0.85); + box-shadow: + 0 7px 28px rgba(0, 0, 0, 0.5), + 0 0 40px rgba(0, 234, 255, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + transform: translateY(-2px); +} + +.btn-sound-main:hover svg { + color: rgba(0, 234, 255, 1); + transform: scale(1.12); +} + +.btn-sound-main:active { + transform: translateY(0); +} + +/* Muted State - Red */ +.btn-sound-main.all-muted { + background: rgba(60, 0, 10, 0.9) !important; + border-color: rgba(255, 60, 60, 0.7) !important; + box-shadow: + 0 5px 20px rgba(0, 0, 0, 0.4), + 0 0 25px rgba(255, 60, 60, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; +} + +.btn-sound-main.all-muted svg { + color: rgba(255, 80, 80, 0.95) !important; +} + +.btn-sound-main.all-muted:hover { + background: rgba(255, 60, 60, 0.2) !important; + border-color: rgba(255, 80, 80, 0.9) !important; +} + +/* Volume Panel */ +.volume-panel { + position: absolute; + top: clamp(46px, 8vh, 60px); + left: 0; + background: linear-gradient(145deg, rgba(20, 0, 40, 0.98), rgba(30, 0, 50, 0.98)); + backdrop-filter: blur(25px); + border: 2px solid rgba(0, 217, 255, 0.4); + border-radius: 18px; + padding: 20px 18px; + min-width: clamp(240px, 30vw, 280px); + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.7), + 0 0 50px rgba(0, 217, 255, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + animation: slideDown 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Volume Item */ +.volume-item { + margin-bottom: 18px; +} + +.volume-item:last-child { + margin-bottom: 0; +} + +.volume-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.volume-icon { + width: 18px; + height: 18px; + color: rgba(0, 234, 255, 0.8); +} + +.volume-label { + font-size: 13px; + font-weight: 700; + color: rgba(255, 255, 255, 0.9); + text-transform: uppercase; + letter-spacing: 0.5px; + flex: 1; +} + +.volume-value { + font-size: 12px; + font-weight: 700; + color: rgba(0, 234, 255, 0.9); + min-width: 40px; + text-align: right; +} + +/* Volume Slider */ +.volume-slider { + width: 100%; + height: 6px; + -webkit-appearance: none; + appearance: none; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + outline: none; + cursor: pointer; + transition: all 0.2s; +} + +.volume-slider::-webkit-slider-track { + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; +} + +.volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: linear-gradient(135deg, #00eaff 0%, #0099ff 100%); + border-radius: 50%; + cursor: pointer; + box-shadow: + 0 2px 10px rgba(0, 234, 255, 0.5), + 0 0 20px rgba(0, 234, 255, 0.3); + transition: all 0.2s; +} + +.volume-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: + 0 4px 15px rgba(0, 234, 255, 0.7), + 0 0 30px rgba(0, 234, 255, 0.5); +} + +.volume-slider::-moz-range-track { + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; +} + +.volume-slider::-moz-range-thumb { + width: 18px; + height: 18px; + background: linear-gradient(135deg, #00eaff 0%, #0099ff 100%); + border: none; + border-radius: 50%; + cursor: pointer; + box-shadow: + 0 2px 10px rgba(0, 234, 255, 0.5), + 0 0 20px rgba(0, 234, 255, 0.3); + transition: all 0.2s; +} + +.volume-slider::-moz-range-thumb:hover { + transform: scale(1.2); +} + +/* Slider Fill Effect */ +.volume-slider { + background: linear-gradient(to right, + rgba(0, 234, 255, 0.3) 0%, + rgba(0, 234, 255, 0.3) var(--value, 0%), + rgba(255, 255, 255, 0.1) var(--value, 0%), + rgba(255, 255, 255, 0.1) 100%); +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .volume-panel { + min-width: clamp(220px, 50vw, 260px); + padding: 16px 14px; + } + + .volume-item { + margin-bottom: 14px; + } + + .volume-label { + font-size: 12px; + } + + .volume-value { + font-size: 11px; + } +} \ No newline at end of file diff --git a/2048.html b/2048.html index c5c2f07..d91f3e8 100644 --- a/2048.html +++ b/2048.html @@ -1,26 +1,239 @@ - - - - - 2048 - - - - - -
-
-

2048

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

2048

+
+
+
SCORE
+
0
+
+
+
HIGH SCORE
+
0
+
+
+
+ + +
+
+ + + + + + + + \ No newline at end of file diff --git a/2048.js b/2048.js index fce07f0..589f60c 100644 --- a/2048.js +++ b/2048.js @@ -1,29 +1,46 @@ -/* 2048.js — Enhanced with animations, particles, and glows - Replace previous 2048.js content with this file. -*/ + /* ------------------------ - 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; +let mergesInCurrentMove = 0; -// --- Audio setup --- +/* ------------------------ + Audio Setup + ------------------------ */ const audio = { - bg: new Audio("bgmusic.mp3"), - pop: new Audio("pop.mp3"), - merge: new Audio("merge.wav") + bg: new Audio("Bgmusic.mp3"), + pop: new Audio("Pop.mp3"), + merge: new Audio("Merge.mp3") }; -audio.bg.volume = 0.25; -audio.pop.volume = 0.9; -audio.merge.volume = 0.9; + +// Sound State (baca dari localStorage atau default ON) +let soundState = { + bg: localStorage.getItem('sound_bg') !== 'false', + pop: localStorage.getItem('sound_pop') !== 'false', + merge: localStorage.getItem('sound_merge') !== 'false' +}; + +// Update audio volumes based on state +function updateAudioVolumes() { + audio.bg.volume = soundState.bg ? 0.25 : 0; + audio.pop.volume = soundState.pop ? 0.9 : 0; + audio.merge.volume = soundState.merge ? 1.0 : 0; +} + audio.bg.loop = true; function tryPlayBg() { + if (!soundState.bg) return; // Jangan play kalau muted + audio.bg.play().catch(() => { const unlock = () => { - audio.bg.play().catch(()=>{}); + if (soundState.bg) audio.bg.play().catch(()=>{}); window.removeEventListener("keydown", unlock); window.removeEventListener("click", unlock); }; @@ -32,25 +49,128 @@ function tryPlayBg() { }); } +/* ===== AUTO TUTORIAL FOR FIRST TIME USERS ===== */ +function checkAndShowTutorial() { + const showTutorial = sessionStorage.getItem("showTutorial"); + const loggedInUser = sessionStorage.getItem("loggedInUser"); + + // Jika user baru (showTutorial = "true"), tampilkan tutorial otomatis + if (showTutorial === "true" && loggedInUser) { + setTimeout(() => { + const tutorialOverlay = document.getElementById('tutorial-overlay'); + if (tutorialOverlay) { + tutorialOverlay.style.display = 'flex'; + } + // Set flag agar tidak muncul lagi di session ini + sessionStorage.setItem("showTutorial", "false"); + }, 500); // Delay 500ms agar halaman sudah fully loaded + } +} + /* ------------------------ - DOM ready + DOM Ready ------------------------ */ document.addEventListener("DOMContentLoaded", () => { + updateBestScoreDisplay(); setupBoard(); addNewTile(); addNewTile(); + updateAudioVolumes(); // Apply saved sound settings tryPlayBg(); document.addEventListener("keydown", handleKey); - setupAmbientCursor(); + setupEventListeners(); + checkAndShowTutorial(); }); /* ------------------------ - 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); + } + + if (gameOverClose) { + gameOverClose.addEventListener('click', hideGameOver); + } + + const gameOverOverlay = document.getElementById('game-over-overlay'); + if (gameOverOverlay) { + gameOverOverlay.addEventListener('click', function(e) { + if (e.target === this) { + hideGameOver(); + } + }); + } + + // Sound Control Buttons + const btnSoundBg = document.getElementById('btn-sound-bg'); + const btnSoundPop = document.getElementById('btn-sound-pop'); + const btnSoundMerge = document.getElementById('btn-sound-merge'); + + if (btnSoundBg) { + btnSoundBg.addEventListener('click', () => toggleSound('bg')); + updateSoundButtonState(btnSoundBg, soundState.bg); + } + + if (btnSoundPop) { + btnSoundPop.addEventListener('click', () => toggleSound('pop')); + updateSoundButtonState(btnSoundPop, soundState.pop); + } + + if (btnSoundMerge) { + btnSoundMerge.addEventListener('click', () => toggleSound('merge')); + updateSoundButtonState(btnSoundMerge, soundState.merge); + } +} + +/* ------------------------ + Setup & Rendering ------------------------ */ function setupBoard() { board = []; - score = 0; - updateScore(); + currentScore = 0; + updateScoreDisplay(); const container = document.getElementById("board"); if (!container) { @@ -71,65 +191,60 @@ function setupBoard() { } } -/* 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(); + 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,27 +262,27 @@ 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 with mute check */ function playSound(soundObj) { try { + // Check if sound is enabled + if (soundObj === audio.pop && !soundState.pop) return; + if (soundObj === audio.merge && !soundState.merge) return; + if (soundObj === audio.bg && !soundState.bg) return; + soundObj.currentTime = 0; soundObj.play().catch(() => {}); } catch (e) {} } /* ------------------------ - Movement helpers (logic preserved) + Movement Logic ------------------------ */ function filterZero(row) { return row.filter(n => n !== 0); @@ -176,136 +291,235 @@ function filterZero(row) { function slide(row) { row = filterZero(row); let mergedThisMove = false; + let mergedPositions = []; + let mergeCount = 0; 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); + + if (navigator.vibrate) { + navigator.vibrate([80, 20, 80]); + } - score += row[i]; + currentScore += row[i]; row[i + 1] = 0; mergedThisMove = true; + mergedPositions.push(i); + mergeCount++; } } row = filterZero(row); while (row.length < 4) row.push(0); - return { row, merged: mergedThisMove }; + return { row, merged: mergedThisMove, mergedPositions, mergeCount }; } function arraysEqual(a, b) { return a.length === b.length && a.every((v, i) => v === b[i]); } +/* Move functions */ function moveLeft() { let moved = false; + let mergedCells = []; + mergesInCurrentMove = 0; + for (let r = 0; r < 4; r++) { - const { row: newRow } = slide(board[r]); + const { row: newRow, mergedPositions, mergeCount } = slide(board[r]); if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; + + mergesInCurrentMove += mergeCount; + + if (mergedPositions && mergedPositions.length > 0) { + mergedPositions.forEach(c => { + mergedCells.push({ r, c }); + }); + } + } + + if (moved) { + refreshBoard(); + triggerComboEffect(mergedCells, mergesInCurrentMove); } - if (moved) updateAfterMove(); return moved; } function moveRight() { let moved = false; + let mergedCells = []; + mergesInCurrentMove = 0; + for (let r = 0; r < 4; r++) { let reversed = [...board[r]].reverse(); - const { row: slid } = slide(reversed); + const { row: slid, mergedPositions, mergeCount } = slide(reversed); let newRow = slid.reverse(); if (!arraysEqual(newRow, board[r])) moved = true; board[r] = newRow; + + mergesInCurrentMove += mergeCount; + + if (mergedPositions && mergedPositions.length > 0) { + mergedPositions.forEach(pos => { + const c = 3 - pos; + mergedCells.push({ r, c }); + }); + } + } + + if (moved) { + refreshBoard(); + triggerComboEffect(mergedCells, mergesInCurrentMove); } - if (moved) updateAfterMove(); return moved; } function moveUp() { let moved = false; + let mergedCells = []; + mergesInCurrentMove = 0; + 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, mergeCount } = slide(col); for (let r = 0; r < 4; r++) { if (board[r][c] !== newCol[r]) moved = true; board[r][c] = newCol[r]; } + + mergesInCurrentMove += mergeCount; + + if (mergedPositions && mergedPositions.length > 0) { + mergedPositions.forEach(r => { + mergedCells.push({ r, c }); + }); + } + } + + if (moved) { + refreshBoard(); + triggerComboEffect(mergedCells, mergesInCurrentMove); } - if (moved) updateAfterMove(); return moved; } function moveDown() { let moved = false; + let mergedCells = []; + mergesInCurrentMove = 0; + 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, mergeCount } = 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]; } + + mergesInCurrentMove += mergeCount; + + if (mergedPositions && mergedPositions.length > 0) { + mergedPositions.forEach(pos => { + const r = 3 - pos; + mergedCells.push({ r, c }); + }); + } + } + + if (moved) { + refreshBoard(); + triggerComboEffect(mergedCells, mergesInCurrentMove); } - 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 ------------------------ */ 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(); } + + 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(); + } + 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"; +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 +527,450 @@ 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 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); +function restartGame() { + hideGameOver(); + resetScore(); + setupBoard(); + addNewTile(); + addNewTile(); + refreshBoard(); + isMoving = false; } -/* 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; +function playAgain() { + restartGame(); +} - 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 goHome() { + try { + audio.bg.pause(); + audio.bg.currentTime = 0; + } catch (e) {} + window.location.href = "Homepage.html"; } /* ------------------------ - 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. + Game Over Modal ------------------------ */ -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 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-block'; + 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'; + } +} + +/* ============================================= + ADVANCED VOLUME CONTROL SYSTEM + ============================================= */ + +// Volume State (0-100 for each sound) +let volumeState = { + music: parseInt(localStorage.getItem('vol_music')) || 25, + pop: parseInt(localStorage.getItem('vol_pop')) || 90, + merge: parseInt(localStorage.getItem('vol_merge')) || 100 +}; + +// Apply volumes on load +function initVolumeControl() { + // Set audio volumes + audio.bg.volume = volumeState.music / 100; + audio.pop.volume = volumeState.pop / 100; + audio.merge.volume = volumeState.merge / 100; + + // Update sliders + const musicSlider = document.getElementById('vol-music'); + const popSlider = document.getElementById('vol-pop'); + const mergeSlider = document.getElementById('vol-merge'); + + if (musicSlider) { + musicSlider.value = volumeState.music; + updateSliderFill(musicSlider, volumeState.music); + document.getElementById('vol-music-display').textContent = volumeState.music + '%'; + } + + if (popSlider) { + popSlider.value = volumeState.pop; + updateSliderFill(popSlider, volumeState.pop); + document.getElementById('vol-pop-display').textContent = volumeState.pop + '%'; + } + + if (mergeSlider) { + mergeSlider.value = volumeState.merge; + updateSliderFill(mergeSlider, volumeState.merge); + document.getElementById('vol-merge-display').textContent = volumeState.merge + '%'; + } + + updateMainSoundIcon(); + + // Event listeners for sliders + if (musicSlider) { + musicSlider.addEventListener('input', (e) => { + const val = parseInt(e.target.value); + volumeState.music = val; + audio.bg.volume = val / 100; + localStorage.setItem('vol_music', val); + document.getElementById('vol-music-display').textContent = val + '%'; + updateSliderFill(e.target, val); + updateMainSoundIcon(); + + // Auto-play BG music if volume > 0 + if (val > 0 && audio.bg.paused) { + tryPlayBg(); + } else if (val === 0) { + audio.bg.pause(); } + }); + } + + if (popSlider) { + popSlider.addEventListener('input', (e) => { + const val = parseInt(e.target.value); + volumeState.pop = val; + audio.pop.volume = val / 100; + localStorage.setItem('vol_pop', val); + document.getElementById('vol-pop-display').textContent = val + '%'; + updateSliderFill(e.target, val); + updateMainSoundIcon(); + }); + } + + if (mergeSlider) { + mergeSlider.addEventListener('input', (e) => { + const val = parseInt(e.target.value); + volumeState.merge = val; + audio.merge.volume = val / 100; + localStorage.setItem('vol_merge', val); + document.getElementById('vol-merge-display').textContent = val + '%'; + updateSliderFill(e.target, val); + updateMainSoundIcon(); + }); + } + + // Toggle panel visibility + const btnSoundMain = document.getElementById('btn-sound-main'); + const volumePanel = document.getElementById('volume-panel'); + + if (btnSoundMain && volumePanel) { + btnSoundMain.addEventListener('click', (e) => { + e.stopPropagation(); + const isVisible = volumePanel.style.display === 'block'; + volumePanel.style.display = isVisible ? 'none' : 'block'; + }); + + // Close panel when clicking outside + document.addEventListener('click', (e) => { + if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target)) { + volumePanel.style.display = 'none'; + } + }); + + // Prevent panel click from closing + volumePanel.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } +} + +// Update slider fill effect +function updateSliderFill(slider, value) { + slider.style.setProperty('--value', value + '%'); +} + +// Update main sound icon based on volumes +function updateMainSoundIcon() { + const btnMain = document.getElementById('btn-sound-main'); + if (!btnMain) return; + + const iconFull = btnMain.querySelector('.sound-full'); + const iconMedium = btnMain.querySelector('.sound-medium'); + const iconLow = btnMain.querySelector('.sound-low'); + const iconMuted = btnMain.querySelector('.sound-muted'); + + // Calculate total volume average + const totalVolume = volumeState.music + volumeState.pop + volumeState.merge; + const avgVolume = totalVolume / 3; + + // Hide all icons first + iconFull.style.display = 'none'; + iconMedium.style.display = 'none'; + iconLow.style.display = 'none'; + iconMuted.style.display = 'none'; + + // Show appropriate icon based on average + if (totalVolume === 0) { + iconMuted.style.display = 'block'; + btnMain.classList.add('all-muted'); + } else { + btnMain.classList.remove('all-muted'); + + if (avgVolume >= 60) { + iconFull.style.display = 'block'; + } else if (avgVolume >= 30) { + iconMedium.style.display = 'block'; + } else { + iconLow.style.display = 'block'; } } } -/* 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"; +// Initialize on DOM load (add this to your existing DOMContentLoaded) +document.addEventListener("DOMContentLoaded", () => { + // ... existing code ... + initVolumeControl(); // ADD THIS LINE +}); + +/* ============================================= + COMBO EFFECTS + ============================================= */ +function triggerComboEffect(mergedCells, comboCount) { + if (mergedCells.length === 0) return; + + mergedCells.forEach(cell => { + const tile = document.getElementById(`${cell.r}-${cell.c}`); + if (!tile) return; + + tile.classList.add('merge'); + setTimeout(() => tile.classList.remove('merge'), 300); + + createParticleBurst(tile); + + tile.style.boxShadow = '0 0 40px currentColor'; + setTimeout(() => { + tile.style.boxShadow = ''; + }, 300); + + const rect = tile.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const tileValue = parseInt(tile.textContent); + createScorePopup(centerX, centerY, tileValue); + }); + + if (comboCount >= 2) { + showComboPopup(comboCount); + } } -/* 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; +function showComboPopup(comboCount) { + const board = document.getElementById('board'); + if (!board) return; + + const rect = board.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const popup = document.createElement('div'); + popup.className = 'combo-popup'; + popup.style.left = centerX + 'px'; + popup.style.top = centerY + 'px'; + popup.style.position = 'fixed'; + popup.style.fontWeight = '900'; + popup.style.pointerEvents = 'none'; + popup.style.zIndex = '9999'; + popup.style.transform = 'translate(-50%, -50%)'; + popup.style.textTransform = 'uppercase'; + popup.style.letterSpacing = '3px'; + + if (comboCount === 2) { + popup.textContent = 'COMBO x2!'; + popup.style.fontSize = '36px'; + popup.style.color = '#00ff99'; + popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)'; + } else if (comboCount === 3) { + popup.textContent = 'AMAZING x3!'; + popup.style.fontSize = '42px'; + popup.style.color = '#ff00ff'; + popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)'; + } else if (comboCount >= 4) { + popup.textContent = 'PERFECT x' + comboCount + '!'; + popup.style.fontSize = '48px'; + popup.style.color = '#ffd700'; + popup.style.textShadow = '0 0 40px rgba(255, 215, 0, 1), 0 0 70px rgba(255, 215, 0, 0.7)'; } - 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]; + + document.body.appendChild(popup); + + popup.animate([ + { + transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', + opacity: 0 + }, + { + transform: 'translate(-50%, -50%) scale(1.3) rotate(5deg)', + opacity: 1, + offset: 0.3 + }, + { + transform: 'translate(-50%, -50%) scale(1.1) rotate(-2deg)', + opacity: 1, + offset: 0.6 + }, + { + transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)', + opacity: 0 } - } - 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; + ], { + duration: 1200, + easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' + }).onfinish = () => popup.remove(); +} + +function createParticleBurst(tileElement) { + const rect = tileElement.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const tileValue = parseInt(tileElement.textContent); + const tileColor = getTileColor(tileValue); + + 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); + + 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; + + 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(); + } +} + +function createScorePopup(x, y, score) { + const popup = document.createElement('div'); + popup.className = 'score-popup'; + popup.textContent = '+' + score; + popup.style.left = x + 'px'; + popup.style.top = y + 'px'; + popup.style.position = 'fixed'; + popup.style.fontSize = '24px'; + popup.style.fontWeight = '900'; + popup.style.color = '#ffd700'; + popup.style.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)'; + popup.style.pointerEvents = 'none'; + popup.style.zIndex = '9999'; + popup.style.transform = 'translate(-50%, -50%)'; + + document.body.appendChild(popup); + + popup.animate([ + { + transform: 'translate(-50%, -50%) scale(0.5)', + opacity: 0 + }, + { + transform: 'translate(-50%, -70px) scale(1.2)', + opacity: 1, + offset: 0.3 + }, + { + transform: 'translate(-50%, -120px) scale(1)', + opacity: 0 + } + ], { + duration: 1000, + easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' + }).onfinish = () => popup.remove(); +} + +function getTileColor(value) { + const colors = { + 2: '#00eaff', + 4: '#00ff99', + 8: '#ff00ff', + 16: '#ff0066', + 32: '#ffaa00', + 64: '#ff0000', + 128: '#5f00ff', + 256: '#00ffea', + 512: '#ff00aa', + 1024: '#00ffaa', + 2048: '#ffd700' + }; + return colors[value] || '#00eaff'; } -/* ------------------------ - End of file - ------------------------ */ diff --git a/Animation_Homepage.js b/Animation_Homepage.js new file mode 100644 index 0000000..b0419d6 --- /dev/null +++ b/Animation_Homepage.js @@ -0,0 +1,66 @@ +// Neon Particles Animation - Optimized for Academic Project +const particlesContainer = document.getElementById('particles'); +const particleCount = 25; // Reduced for better performance + +function createParticle() { + const particle = document.createElement('div'); + particle.className = 'particle'; + + // Random position + particle.style.left = Math.random() * 100 + '%'; + particle.style.top = Math.random() * 100 + '%'; + + // Random animation duration + const duration = 20 + Math.random() * 20; + particle.style.animationDuration = duration + 's'; + + // Random delay + particle.style.animationDelay = Math.random() * 10 + 's'; + + // Random size variation (smaller) + const size = 3 + Math.random() * 3; + particle.style.width = size + 'px'; + particle.style.height = size + 'px'; + + // Random colors (neon theme) + const colors = [ + 'radial-gradient(circle, #00ffff, #0099ff)', + 'radial-gradient(circle, #ff00ff, #cc00ff)', + 'radial-gradient(circle, #00ff99, #00cc77)' + ]; + particle.style.background = colors[Math.floor(Math.random() * colors.length)]; + + // Add floating animation + particle.style.animation = `floatParticle ${duration}s ease-in-out infinite`; + + particlesContainer.appendChild(particle); +} + +// Create particles +for (let i = 0; i < particleCount; i++) { + createParticle(); +} + +// Add CSS animation dynamically +const style = document.createElement('style'); +style.textContent = ` + @keyframes floatParticle { + 0%, 100% { + transform: translate(0, 0) scale(1); + opacity: 0.3; + } + 25% { + transform: translate(15px, -25px) scale(1.1); + opacity: 0.6; + } + 50% { + transform: translate(-10px, -50px) scale(0.9); + opacity: 0.4; + } + 75% { + transform: translate(20px, -35px) scale(1.05); + opacity: 0.5; + } + } +`; +document.head.appendChild(style); \ No newline at end of file diff --git a/Bgmusic.mp3 b/Bgmusic.mp3 new file mode 100644 index 0000000..4481143 Binary files /dev/null and b/Bgmusic.mp3 differ diff --git a/Homepage.css b/Homepage.css new file mode 100644 index 0000000..386cf74 --- /dev/null +++ b/Homepage.css @@ -0,0 +1,436 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: radial-gradient(circle at 20% 20%, #3b0066, #0c001a 70%); + color: #fff; + overflow-x: hidden; + min-height: 100vh; + position: relative; +} + +/* Neon Particles - MATCHING LOGIN/REGISTER */ +#particles { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; +} + +.particle { + position: absolute; + width: 5px; + height: 5px; + border-radius: 50%; + background: radial-gradient(circle, #00ffff, #0099ff); + box-shadow: 0 0 10px #00eaff, 0 0 25px #0088ff; + pointer-events: none; +} + +/* Header - MATCHING LOGIN/REGISTER THEME */ +header { + position: relative; + z-index: 10; + padding: 20px 40px; + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(20, 0, 40, 0.65); + backdrop-filter: blur(10px); + border-bottom: 2px solid rgba(0, 234, 255, 0.3); +} + +.logo { + font-size: 28px; + font-weight: 900; + background: linear-gradient(90deg, #00d9ff 0%, #ff00ff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 3px; + text-transform: uppercase; +} + +.nav-buttons { + display: flex; + gap: 15px; +} + +.btn { + padding: 12px 28px; + border: none; + border-radius: 12px; + font-weight: 700; + font-size: 16px; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + display: inline-block; + text-align: center; + text-transform: uppercase; + letter-spacing: 2px; +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.08); + color: #fff; + border: 2px solid rgba(0, 175, 255, 0.3); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.15); + border-color: #00eaff; + box-shadow: 0 0 20px rgba(0, 234, 255, 0.4); + transform: translateY(-2px); +} + +.btn-primary { + background: linear-gradient(90deg, #00eaff, #ff00ff); + color: #fff; + font-weight: 900; + box-shadow: 0 5px 25px rgba(0, 217, 255, 0.4); +} + +.btn-primary:hover { + box-shadow: 0 8px 35px rgba(0, 217, 255, 0.7); + transform: translateY(-3px); +} + +/* Hero Section - IMPROVED TITLE */ +.hero { + position: relative; + z-index: 10; + text-align: center; + padding: 100px 20px 80px; + max-width: 1000px; + margin: 0 auto; +} + +.hero-title { + font-size: 120px; + font-weight: 900; + margin-bottom: 15px; + background: linear-gradient(90deg, #00d9ff 0%, #ff00ff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 12px; + text-transform: uppercase; + font-family: 'Arial Black', 'Arial Bold', sans-serif; + text-shadow: 0 0 80px rgba(0, 234, 255, 0.4); + filter: drop-shadow(0 0 30px rgba(255, 0, 255, 0.6)); + animation: titleGlow 3s ease-in-out infinite alternate; + line-height: 1.1; +} + +@keyframes titleGlow { + 0% { + filter: drop-shadow(0 0 20px rgba(0, 234, 255, 0.6)) + drop-shadow(0 0 40px rgba(0, 234, 255, 0.4)); + } + 100% { + filter: drop-shadow(0 0 30px rgba(255, 0, 255, 0.8)) + drop-shadow(0 0 60px rgba(255, 0, 255, 0.5)); + } +} + +.hero-subtitle { + font-size: 20px; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 45px; + font-weight: 400; + letter-spacing: 4px; + text-transform: uppercase; +} + +.cta-buttons { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; +} + +.btn-cta { + padding: 16px 40px; + font-size: 18px; + border-radius: 12px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 2px; + border: none; +} + +.btn-play { + background: linear-gradient(90deg, #ff00ff, #ff0066); + box-shadow: 0 5px 30px rgba(255, 0, 255, 0.5); + color: #fff; +} + +.btn-play:hover { + transform: translateY(-3px); + box-shadow: 0 8px 45px rgba(255, 0, 255, 0.8); +} + +.btn-leaderboard { + background: linear-gradient(90deg, #ffd700, #ffaa00); + color: #0c001a; + box-shadow: 0 5px 30px rgba(255, 215, 0, 0.5); +} + +.btn-leaderboard:hover { + transform: translateY(-3px); + box-shadow: 0 8px 45px rgba(255, 215, 0, 0.8); +} + +/* Section Title - MATCHING THEME */ +.section-title { + position: relative; + z-index: 10; + text-align: center; + font-size: 36px; + margin-bottom: 50px; + background: linear-gradient(90deg, #00d9ff 0%, #ff00ff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-transform: uppercase; + letter-spacing: 3px; + font-weight: 900; +} + +/* Features Section */ +.features { + position: relative; + z-index: 10; + max-width: 1200px; + margin: 80px auto; + padding: 0 40px; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 25px; +} + +.feature-card { + background: rgba(30, 0, 50, 0.5); + border: 2px solid rgba(0, 234, 255, 0.25); + border-radius: 20px; + padding: 35px 25px; + text-align: center; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.feature-card:hover { + transform: translateY(-8px); + border-color: #00eaff; + box-shadow: 0 10px 35px rgba(0, 234, 255, 0.4); + background: rgba(30, 0, 50, 0.7); +} + +.feature-icon { + font-size: 50px; + margin-bottom: 18px; + filter: drop-shadow(0 0 15px currentColor); +} + +.feature-card h3 { + font-size: 20px; + margin-bottom: 12px; + font-weight: 800; + color: #00eaff; + text-transform: uppercase; + letter-spacing: 1px; +} + +.feature-card p { + font-size: 15px; + color: rgba(255, 255, 255, 0.7); + line-height: 1.6; +} + +/* How to Play Section */ +.how-to-play { + position: relative; + z-index: 10; + max-width: 1200px; + margin: 80px auto; + padding: 0 40px; +} + +.steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 25px; +} + +.step { + background: rgba(30, 0, 50, 0.5); + border: 2px solid rgba(0, 234, 255, 0.25); + border-radius: 20px; + padding: 35px 25px; + text-align: center; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.step:hover { + transform: translateY(-8px); + border-color: #00eaff; + box-shadow: 0 10px 35px rgba(0, 234, 255, 0.4); +} + +.step-number { + width: 60px; + height: 60px; + margin: 0 auto 20px; + border-radius: 50%; + background: linear-gradient(135deg, #00eaff, #ff00ff); + color: #fff; + font-size: 28px; + font-weight: 900; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 25px rgba(0, 234, 255, 0.6); +} + +.step h3 { + font-size: 20px; + margin-bottom: 12px; + color: #00eaff; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 800; +} + +.step p { + font-size: 15px; + color: rgba(255, 255, 255, 0.7); + line-height: 1.6; +} + +/* Footer */ +footer { + position: relative; + z-index: 10; + text-align: center; + padding: 40px 20px; + margin-top: 100px; + background: rgba(20, 0, 40, 0.65); + backdrop-filter: blur(10px); + border-top: 2px solid rgba(0, 234, 255, 0.3); +} + +footer p { + color: rgba(255, 255, 255, 0.6); + font-size: 15px; +} + +/* Responsive */ +@media (max-width: 1024px) { + .hero-title { + font-size: 90px; + letter-spacing: 8px; + } +} + +@media (max-width: 768px) { + header { + padding: 15px 20px; + flex-direction: column; + gap: 15px; + } + + .logo { + font-size: 24px; + } + + .hero { + padding: 70px 20px 60px; + } + + .hero-title { + font-size: 64px; + letter-spacing: 6px; + } + + .hero-subtitle { + font-size: 16px; + letter-spacing: 2px; + } + + .btn-cta { + padding: 14px 32px; + font-size: 16px; + } + + .features, + .how-to-play { + padding: 0 20px; + margin: 60px auto; + } + + .section-title { + font-size: 28px; + margin-bottom: 35px; + } + + .features-grid, + .steps { + grid-template-columns: 1fr; + gap: 20px; + } +} + +@media (max-width: 480px) { + .hero { + padding: 50px 15px 40px; + } + + .hero-title { + font-size: 48px; + letter-spacing: 4px; + } + + .hero-subtitle { + font-size: 14px; + letter-spacing: 2px; + margin-bottom: 35px; + } + + .btn-cta { + padding: 13px 28px; + font-size: 15px; + } + + .section-title { + font-size: 24px; + letter-spacing: 2px; + } + + .feature-card, + .step { + padding: 28px 20px; + } + + .feature-icon { + font-size: 45px; + } +} + +@media (max-width: 360px) { + .hero-title { + font-size: 40px; + letter-spacing: 3px; + } +} \ No newline at end of file diff --git a/Homepage.html b/Homepage.html new file mode 100644 index 0000000..8525e36 --- /dev/null +++ b/Homepage.html @@ -0,0 +1,46 @@ + + + + + + 2048 + + +
+
+

2048

+
+ +
+
+
Score
+
0
+
+
+
High
+
0
+
+
+ +
+ + +
+ +
+ +
+

How to Play

+

Use arrow keys (↑ ↓ ← →) or swipe to move tiles. When two tiles with the same number touch, they merge into one! Join the numbers to reach the 2048 tile!

+
+
+ +
+
+

Game Over!

+

Your score: 0

+ +
+
+ + \ No newline at end of file diff --git a/Homepage.js b/Homepage.js new file mode 100644 index 0000000..e69de29 diff --git a/Leaderboard.css b/Leaderboard.css index 3793923..3598ad2 100644 --- a/Leaderboard.css +++ b/Leaderboard.css @@ -101,6 +101,33 @@ h1::before { display: flex; flex-direction: column; gap: 14px; + + /* Scroll settings */ + max-height: 500px; + overflow-y: auto; + padding-right: 10px; +} + +/* Custom Scrollbar Styling */ +.leaderboard-list::-webkit-scrollbar { + width: 10px; +} + +.leaderboard-list::-webkit-scrollbar-track { + background: rgba(30, 0, 50, 0.5); + border-radius: 10px; + border: 1px solid rgba(0, 255, 255, 0.1); +} + +.leaderboard-list::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, #00eaff, #ff00ff); + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 234, 255, 0.5); +} + +.leaderboard-list::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, #00ffff, #ff66ff); + box-shadow: 0 0 15px rgba(0, 234, 255, 0.8); } /* ========== LEADERBOARD ITEM ========== */ diff --git a/Leaderboard.html b/Leaderboard.html index 55cc7a6..5418e1c 100644 --- a/Leaderboard.html +++ b/Leaderboard.html @@ -23,8 +23,8 @@
--
-
Top Score
-
0
+
High Score
+
0
@@ -33,8 +33,7 @@
  • 1
    -
    CyberKing
    -
    Level 99
    +
    Player 1
    9,850
    @@ -45,8 +44,7 @@
  • 2
    -
    NeonMaster
    -
    Level 87
    +
    Player 2
    8,200
    @@ -57,8 +55,7 @@
  • 3
    -
    PixelHunter
    -
    Level 75
    +
    Player 3
    6,950
    @@ -69,8 +66,7 @@
  • 4
    -
    StarGazer
    -
    Level 62
    +
    Player 4
    5,420
    @@ -81,8 +77,7 @@
  • 5
    -
    CodeNinja
    -
    Level 58
    +
    Player 5
    4,890
    @@ -90,14 +85,68 @@
  • +
  • +
    6
    +
    +
    Player 6
    +
    +
    +
    4,320
    +
    Points
    +
    +
  • + +
  • +
    7
    +
    +
    Player 7
    +
    +
    +
    3,850
    +
    Points
    +
    +
  • + +
  • +
    8
    +
    +
    Player 8
    +
    +
    +
    3,120
    +
    Points
    +
    +
  • + +
  • +
    9
    +
    +
    Player 9
    +
    +
    +
    2,780
    +
    Points
    +
    +
  • + +
  • +
    10
    +
    +
    Player 10
    +
    +
    +
    2,340
    +
    Points
    +
    +
  • +
  • 12
    You
    -
    Level 42
    -
    3,120
    +
    1,840
    Points
  • @@ -107,5 +156,7 @@ Back to Game + + \ No newline at end of file diff --git a/Leaderboard.js b/Leaderboard.js index e69de29..67aa8ec 100644 --- a/Leaderboard.js +++ b/Leaderboard.js @@ -0,0 +1,243 @@ +// ========== PARTICLES ANIMATION ========== +const particlesContainer = document.getElementById('particles'); + +function createParticle() { + const particle = document.createElement('div'); + particle.className = 'particle'; + + + particle.style.left = Math.random() * 100 + '%'; + particle.style.top = Math.random() * 100 + '%'; + + + const size = 3 + Math.random() * 4; + particle.style.width = size + 'px'; + particle.style.height = size + 'px'; + + const duration = 3 + Math.random() * 5; + const delay = Math.random() * 2; + const moveX = (Math.random() - 0.5) * 200; + + const animationName = `float-${Date.now()}-${Math.random()}`; + const keyframes = ` + @keyframes ${animationName} { + 0% { + transform: translateY(0) translateX(0); + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(-100vh) translateX(${moveX}px); + opacity: 0; + } + } + `; + + const style = document.createElement('style'); + style.innerHTML = keyframes; + document.head.appendChild(style); + + particle.style.animation = `${animationName} ${duration}s ${delay}s ease-in-out forwards`; + + particlesContainer.appendChild(particle); + + setTimeout(() => { + particle.remove(); + style.remove(); + }, (duration + delay) * 1000); +} + +setInterval(createParticle, 300); + +for (let i = 0; i < 25; i++) { + setTimeout(createParticle, i * 100); +} + +// ========== LEADERBOARD DATA MANAGEMENT ========== + +// Get leaderboard data from localStorage +function getLeaderboardData() { + const data = localStorage.getItem('leaderboard2048'); + return data ? JSON.parse(data) : []; +} + +// Save leaderboard data to localStorage +function saveLeaderboardData(data) { + localStorage.setItem('leaderboard2048', JSON.stringify(data)); +} + +// Get current logged-in user +function getCurrentUser() { + return localStorage.getItem('currentUser') || null; +} + +// Add or update score for a player +function addScore(playerName, score) { + let leaderboard = getLeaderboardData(); + + // Find if player already exists + const existingIndex = leaderboard.findIndex(p => p.name === playerName); + + if (existingIndex >= 0) { + // Update only if new score is higher + if (score > leaderboard[existingIndex].score) { + leaderboard[existingIndex].score = score; + leaderboard[existingIndex].level = Math.floor(score / 100); + leaderboard[existingIndex].date = new Date().toISOString(); + } + } else { + // Add new player + leaderboard.push({ + name: playerName, + score: score, + level: Math.floor(score / 100), + date: new Date().toISOString() + }); + } + + saveLeaderboardData(leaderboard); +} + +// ========== RENDER LEADERBOARD ========== +function renderLeaderboard() { + let leaderboardData = getLeaderboardData(); + const currentUser = getCurrentUser(); + const list = document.getElementById('leaderboardList'); + const emptyState = document.getElementById('emptyState'); + + // Sort by score (highest first) + leaderboardData.sort((a, b) => b.score - a.score); + + // Update stats + const totalPlayers = leaderboardData.length; + const topScore = leaderboardData.length > 0 ? leaderboardData[0].score : 0; + + document.getElementById('totalPlayers').textContent = totalPlayers; + document.getElementById('topScore').textContent = topScore.toLocaleString(); + + // Find current user's rank + let userRank = '--'; + if (currentUser) { + const userIndex = leaderboardData.findIndex(p => p.name === currentUser); + if (userIndex >= 0) { + userRank = userIndex + 1; + } + } + document.getElementById('yourRank').textContent = userRank; + + // Check if there's data + if (leaderboardData.length === 0) { + list.style.display = 'none'; + if (emptyState) emptyState.style.display = 'block'; + return; + } + + list.style.display = 'flex'; + if (emptyState) emptyState.style.display = 'none'; + + // Render top 10 players + const top10 = leaderboardData.slice(0, 10); + + list.innerHTML = top10.map((player, index) => { + const rank = index + 1; + let rankClass = 'rank-other'; + + if (rank === 1) rankClass = 'rank-1'; + else if (rank === 2) rankClass = 'rank-2'; + else if (rank === 3) rankClass = 'rank-3'; + + const isCurrentUser = currentUser && player.name === currentUser; + const yourRankClass = isCurrentUser ? 'your-rank' : ''; + + return ` +
  • +
    ${rank}
    +
    +
    ${escapeHtml(player.name)}
    +
    Level ${player.level || Math.floor(player.score / 100)}
    +
    +
    +
    ${player.score.toLocaleString()}
    +
    Points
    +
    +
  • + `; + }).join(''); +} + +// Escape HTML to prevent XSS +function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, m => map[m]); +} + +// ========== SAMPLE DATA (FOR TESTING) ========== +function initSampleData() { + const existingData = getLeaderboardData(); + + // Only add sample data if leaderboard is empty + if (existingData.length === 0) { + const sampleData = [ + { name: "CyberKing", score: 9850, level: 98, date: new Date().toISOString() }, + { name: "NeonMaster", score: 8200, level: 82, date: new Date().toISOString() }, + { name: "PixelHunter", score: 6950, level: 69, date: new Date().toISOString() }, + { name: "StarGazer", score: 5420, level: 54, date: new Date().toISOString() }, + { name: "CodeNinja", score: 4890, level: 48, date: new Date().toISOString() }, + { name: "ByteWarrior", score: 4320, level: 43, date: new Date().toISOString() }, + { name: "DataDragon", score: 3850, level: 38, date: new Date().toISOString() }, + { name: "SyntaxSage", score: 3120, level: 31, date: new Date().toISOString() }, + { name: "LogicLord", score: 2780, level: 27, date: new Date().toISOString() }, + { name: "BugSlayer", score: 2340, level: 23, date: new Date().toISOString() } + ]; + + saveLeaderboardData(sampleData); + console.log('Sample data initialized'); + } +} + +// ========== CLEAR LEADERBOARD (FOR TESTING) ========== +function clearLeaderboard() { + if (confirm('Are you sure you want to clear all leaderboard data?')) { + localStorage.removeItem('leaderboard2048'); + renderLeaderboard(); + console.log('Leaderboard cleared'); + } +} + +// ========== REFRESH LEADERBOARD ========== +function refreshLeaderboard() { + renderLeaderboard(); + console.log('Leaderboard refreshed'); +} + +// ========== INITIALIZE ON PAGE LOAD ========== +window.addEventListener('DOMContentLoaded', function() { + + renderLeaderboard(); + + console.log('Leaderboard initialized'); + console.log('Available functions:'); + console.log('- addScore(name, score) - Add/update a score'); + console.log('- clearLeaderboard() - Clear all data'); + console.log('- refreshLeaderboard() - Refresh display'); +}); + +// ========== EXPOSE FUNCTIONS TO WINDOW (FOR EXTERNAL USE) ========== +window.leaderboard = { + addScore: addScore, + getLeaderboardData: getLeaderboardData, + clearLeaderboard: clearLeaderboard, + refreshLeaderboard: refreshLeaderboard, + initSampleData: initSampleData +}; \ No newline at end of file diff --git a/Merge.mp3 b/Merge.mp3 new file mode 100644 index 0000000..ecfe0c4 Binary files /dev/null and b/Merge.mp3 differ diff --git a/Modal_Register.js b/Modal_Register.js index 9a18417..6334895 100644 --- a/Modal_Register.js +++ b/Modal_Register.js @@ -9,10 +9,10 @@ export function showModal(type, title, message) { if (type === "success") { modalIcon.classList.add("success"); - modalTitle.textContent = title || "Register Berhasil!"; + modalTitle.textContent = title || "Register Successful!"; } else { modalIcon.classList.add("error"); - modalTitle.textContent = title || "Register Gagal!"; + modalTitle.textContent = title || "Register Failed!"; } modalMessage.textContent = message; diff --git a/Pop.mp3 b/Pop.mp3 new file mode 100644 index 0000000..28b742f Binary files /dev/null and b/Pop.mp3 differ diff --git a/Register.css b/Register.css index 7a5cb53..8c5504f 100644 --- a/Register.css +++ b/Register.css @@ -158,14 +158,18 @@ input::placeholder { text-shadow: 0 0 10px #00ddff; } -/* Modal */ +/* Modal - CENTERED VERSION */ .modal { display: none; position: fixed; - z-index: 999; - inset: 0; - background-color: rgba(0, 0, 0, 0.75); - backdrop-filter: blur(8px); + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + animation: fadeIn 0.3s ease; } .modal.show { @@ -175,75 +179,124 @@ input::placeholder { } .modal-content { - background: rgba(20, 0, 40, 0.95); + background: linear-gradient(135deg, rgba(30, 0, 50, 0.95) 0%, rgba(50, 0, 80, 0.95) 100%); + border: 2px solid rgba(0, 217, 255, 0.3); border-radius: 20px; - border: 2px solid rgba(0, 255, 255, 0.4); - padding: 35px 25px; - width: 90%; + padding: 40px 30px; + text-align: center; max-width: 400px; + width: 90%; + 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: slideIn 0.3s ease, glowBorder 3s ease-in-out infinite; +} - box-shadow: - 0 0 30px #00eaff, - 0 0 60px #ff00ff; +@keyframes glowBorder { + 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); + } } .modal-icon { width: 80px; height: 80px; - margin: 0 auto 15px; + margin: 0 auto 20px; border-radius: 50%; - display: flex; justify-content: center; align-items: center; - font-size: 38px; - color: white; + font-size: 40px; } .modal-icon.success { - background: linear-gradient(135deg, #00eaff, #00ff88); - box-shadow: 0 0 25px #00ffcc; + background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%); + box-shadow: 0 0 30px rgba(0, 217, 255, 0.6); +} + +.modal-icon.success::before { + content: "✓"; + color: #fff; } .modal-icon.error { - background: linear-gradient(135deg, #ff0066, #ff00ff); - box-shadow: 0 0 25px #ff00aa; + background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%); + box-shadow: 0 0 30px rgba(255, 0, 128, 0.6); +} + +.modal-icon.error::before { + content: "✕"; + color: #fff; } .modal-content h2 { - font-size: 1.7rem; - margin-bottom: 10px; - - background: linear-gradient(90deg, #00eaff, #ff00ff); + color: #fff; + font-size: 1.8rem; + margin-bottom: 15px; + background: linear-gradient(90deg, #00d9ff 0%, #ff00ff 100%); -webkit-background-clip: text; - background-clip: text; /* FIX: properti standar */ -webkit-text-fill-color: transparent; + background-clip: text; } .modal-content p { - color: rgba(240, 240, 255, 0.8); - margin-bottom: 25px; + color: rgba(255, 255, 255, 0.8); + font-size: 1rem; + margin-bottom: 30px; line-height: 1.5; } .modal-btn { - padding: 12px 55px; - background: linear-gradient(90deg, #00eaff, #ff00ff); + padding: 12px 50px; + background: linear-gradient(90deg, #00d9ff 0%, #ff00ff 100%); border: none; border-radius: 10px; - - color: white; + color: #fff; font-size: 16px; font-weight: bold; - + text-transform: uppercase; + letter-spacing: 2px; cursor: pointer; - box-shadow: 0 0 20px #00d9ff; - transition: 0.3s ease; + transition: all 0.3s ease; + box-shadow: 0 5px 25px rgba(0, 217, 255, 0.4); } .modal-btn:hover { - transform: translateY(-3px); - box-shadow: 0 0 30px #00eaff; + transform: translateY(-2px); + box-shadow: 0 8px 35px rgba(0, 217, 255, 0.6); +} + +.modal-btn:active { + transform: translateY(0); +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateY(-50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } } /* Autocomplete/Datalist Styling */ @@ -251,7 +304,7 @@ input::-webkit-calendar-picker-indicator { filter: invert(1); } -/* Styling untuk browser autocomplete */ +/* Browser autocomplete styling */ input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus { @@ -265,7 +318,7 @@ input:-webkit-autofill:focus { border-color: #00d9ff; } -/* Styling untuk datalist dropdown - terbatas karena browser restrictions */ +/* Datalist dropdown styling - limited due to browser restrictions */ datalist { background-color: rgba(30, 0, 50, 0.95); } @@ -276,12 +329,12 @@ option { padding: 10px; } -/* Untuk Firefox */ +/* For Firefox */ input::-moz-list-thumbnail { background-color: rgba(30, 0, 50, 0.95); } -/* Custom styling untuk dropdown suggestion */ +/* Custom styling for dropdown suggestion */ input[list]::-webkit-list-button { color: #00d9ff; } @@ -305,4 +358,18 @@ input[list]::-webkit-list-button { padding: 13px; font-size: 16px; } -} + + .modal-icon { + width: 60px; + height: 60px; + font-size: 30px; + } + + .modal-content h2 { + font-size: 1.3rem; + } + + .modal-content p { + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/Register.js b/Register.js index 6cda798..1eab730 100644 --- a/Register.js +++ b/Register.js @@ -14,13 +14,13 @@ document.getElementById("registerForm").addEventListener("submit", async functio // Validasi kosong if (!username || !password || !confirmPassword) { - showModal("error", "Register Gagal!", "Semua field wajib diisi."); + showModal("error", "Register Failed!", "All fields are required."); return; } // Validasi password tidak sama if (password !== confirmPassword) { - showModal("error", "Password Tidak Cocok!", "Password dan Confirm Password harus sama."); + showModal("error", "Password Mismatch!", "Password and Confirm Password must match."); setInputError("password"); setInputError("confirmPassword"); return; @@ -29,6 +29,7 @@ document.getElementById("registerForm").addEventListener("submit", async functio // Button loading state const submitBtn = this.querySelector("button[type='submit']"); const originalText = submitBtn.textContent; + submitBtn.disabled = true; try { @@ -36,20 +37,20 @@ document.getElementById("registerForm").addEventListener("submit", async functio // API temenmu return: { status: "success", message: "..." } if (data.status === "success") { - showModal("success", "Register Berhasil!", data.message); + showModal("success", "Register Successful!", data.message); } else { - showModal("error", "Register Gagal!", data.message || "Terjadi kesalahan."); + showModal("error", "Register Failed!", data.message || "An error occurred."); setInputError("username"); } } catch (error) { console.error("Register Error:", error); - let msg = "Terjadi kesalahan koneksi."; + let msg = "A connection error occurred."; if (error.message === "Failed to fetch") { - msg = "Tidak dapat terhubung ke server."; + msg = "Unable to connect to server."; } showModal("error", "Error!", msg);