diff --git a/2048.css b/2048.css index 40524ca..e8742e9 100644 --- a/2048.css +++ b/2048.css @@ -23,7 +23,6 @@ body { position: relative; } -/* ADDED: Animated Grid Pattern (optional, bisa dihapus kalau ga suka) */ body::before { content: ''; position: fixed; @@ -49,7 +48,6 @@ body::before { } } -/* ADDED: Floating gradient blobs for depth */ body::after { content: ''; position: fixed; @@ -90,313 +88,6 @@ body::after { transform: scale(0.95); } -/* ====================== - HEADER - All Centered Vertically -====================== */ -.game-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 20px; - width: 100%; -} - -h1 { - font-size: 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); -} - -/* FORCE tombol sound untuk selalu visible */ -.btn-sound-main { - display: flex !important; - position: relative !important; - z-index: 103 !important; -} - -.volume-panel { - position: relative !important; - z-index: 102 !important; -} - - -.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); @@ -642,93 +333,6 @@ h1 { } } -/* ========================== - 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; @@ -837,209 +441,4 @@ h1 { color: #ffd700; text-shadow: 0 0 10px rgba(255, 215, 0, 0.6); font-weight: 800; -} - -/* ========================== - GAME OVER MODAL - WITH ICON BUTTONS -========================== */ - -.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); - } -} - -/* High Score Display - ORANGE gradient */ -.high-score-display { - margin-top: 28px; - padding-top: 28px; - border-top: 2px solid rgba(255, 140, 0, 0.25); -} - -.high-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; -} - -.high-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; } \ No newline at end of file diff --git a/2048.html b/2048.html index 95e6002..02cda18 100644 --- a/2048.html +++ b/2048.html @@ -4,12 +4,14 @@ 2048 + + - - + + diff --git a/2048_Background_Effects.css b/2048_Background_Effects.css index e0b6d0a..1f11203 100644 --- a/2048_Background_Effects.css +++ b/2048_Background_Effects.css @@ -1,6 +1,3 @@ -/* ========================== - BACKGROUND EFFECTS -========================== */ .particles { position: fixed; inset: 0; @@ -68,8 +65,7 @@ will-change: transform, opacity; filter: blur(1px) drop-shadow(0 0 8px rgba(255,255,255,0.1)); } -/* ENHANCED BACKGROUND EFFECTS */ -/* Update .particles styling */ + .particles { position: fixed; inset: 0; @@ -100,7 +96,6 @@ } } -/* Enhanced Starfield */ .starfield { position: fixed; inset: 0; @@ -131,7 +126,6 @@ } } -/* Enhanced Cursor Light */ .cursor-light { position: absolute; width: 280px; diff --git a/2048_Header.css b/2048_Header.css new file mode 100644 index 0000000..a508e3a --- /dev/null +++ b/2048_Header.css @@ -0,0 +1,71 @@ +.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 { + 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 { + position: fixed; + top: clamp(10px, 2vh, 20px); + right: clamp(10px, 2vw, 20px); + display: flex; + gap: clamp(8px, 1.5vw, 12px); + z-index: 100; +} \ No newline at end of file diff --git a/2048_Modal.css b/2048_Modal.css new file mode 100644 index 0000000..6db4d30 --- /dev/null +++ b/2048_Modal.css @@ -0,0 +1,288 @@ +.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; +} + +/* ========================== + GAME OVER MODAL - WITH ICON BUTTONS +========================== */ + +.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); + } +} + +/* High Score Display - ORANGE gradient */ +.high-score-display { + margin-top: 28px; + padding-top: 28px; + border-top: 2px solid rgba(255, 140, 0, 0.25); +} + +.high-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; +} + +.high-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; +} \ No newline at end of file diff --git a/2048_Sound.css b/2048_Sound.css index 3ef6521..5f89e7f 100644 --- a/2048_Sound.css +++ b/2048_Sound.css @@ -1,6 +1,231 @@ -/* ========================== - ADVANCED SOUND CONTROL -========================== */ +.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); +} + +/* FORCE tombol sound untuk selalu visible */ +.btn-sound-main { + display: flex !important; + position: relative !important; + z-index: 103 !important; +} + +.volume-panel { + position: relative !important; + z-index: 102 !important; +} + + +.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); +} + .sound-control-container { position: fixed; top: clamp(10px, 2vh, 20px); diff --git a/2048_Tutorial_Logic.js b/2048_Tutorial_Logic.js index a22dd83..a19f040 100644 --- a/2048_Tutorial_Logic.js +++ b/2048_Tutorial_Logic.js @@ -1,44 +1,40 @@ function checkAndShowTutorial() { - // Ambil user terbaru saat fungsi dijalankan + // Ambil user yang sedang login (atau guest) const currentUser = sessionStorage.getItem("loggedInUser") || "guest"; const tutorialKey = 'tutorialSeen_' + currentUser; - // Cek di Console browser (tekan F12 -> Console) console.log(`[Tutorial Check] User: ${currentUser}`); console.log(`[Tutorial Check] Key: ${tutorialKey}`); - - // Cek status di LocalStorage + + // Cek apakah tutorial sudah pernah dilihat const hasSeenTutorial = localStorage.getItem(tutorialKey); console.log(`[Tutorial Check] Status Seen: ${hasSeenTutorial}`); const tutorialOverlay = document.getElementById('tutorial-overlay'); - // Logic: Jika belum pernah lihat (null) -> Tampilkan + // Tampilkan tutorial jika belum pernah dilihat if (!hasSeenTutorial && tutorialOverlay) { - console.log("-> Menampilkan Tutorial untuk user baru."); tutorialOverlay.style.display = 'flex'; - } else { - console.log("-> User ini sudah pernah lihat tutorial (atau overlay tidak ketemu)."); } } -// Jalankan otomatis saat halaman selesai loading +// Jalankan saat halaman selesai dimuat document.addEventListener('DOMContentLoaded', () => { - checkAndShowTutorial(); + checkAndShowTutorial(); // Cek & tampilkan tutorial - // Setup tombol close hanya sekali const closeTutorialBtn = document.getElementById('close-tutorial'); const tutorialOverlay = document.getElementById('tutorial-overlay'); - + + // Event klik tombol tutup tutorial if (closeTutorialBtn) { closeTutorialBtn.addEventListener('click', () => { - // Ambil user SAAT INI (penting jika user berubah tanpa reload) + // Ambil user aktif saat ini const currentUser = sessionStorage.getItem("loggedInUser") || "guest"; const tutorialKey = 'tutorialSeen_' + currentUser; - - if(tutorialOverlay) tutorialOverlay.style.display = 'none'; + + // Sembunyikan tutorial & simpan status + if (tutorialOverlay) tutorialOverlay.style.display = 'none'; localStorage.setItem(tutorialKey, 'true'); - console.log(`[Tutorial Check] Disimpan: ${tutorialKey} = true`); }); } }); diff --git a/Connection.php b/Connection.php index b8b01b2..9e10452 100644 --- a/Connection.php +++ b/Connection.php @@ -1,7 +1,11 @@ connect_error) { http_response_code(500); diff --git a/Homepage.html b/Homepage.html index 197b658..b73ae04 100644 --- a/Homepage.html +++ b/Homepage.html @@ -6,6 +6,7 @@ Homepage + diff --git a/Homepage_Responsive.css b/Homepage_Responsive.css index 1fc9e1a..0ffa0af 100644 --- a/Homepage_Responsive.css +++ b/Homepage_Responsive.css @@ -75,4 +75,26 @@ .btn-logout-confirm { width: 100%; } + + /* --- GANTI BAGIAN SCROLLBAR DENGAN INI --- */ + + /* Tunjuk langsung HTML & Body biar browser gak bingung */ + html::-webkit-scrollbar, + body::-webkit-scrollbar { + width: 6px !important; /* Pakai !important biar maksa */ + background: #0b0b15; + } + + html::-webkit-scrollbar-track, + body::-webkit-scrollbar-track { + background: #0b0b15; + } + + html::-webkit-scrollbar-thumb, + body::-webkit-scrollbar-thumb { + background-color: #bc13fe !important; /* Warna Magenta */ + border-radius: 10px; + border: none; + box-shadow: 0 0 8px #bc13fe !important; /* Glow Neon */ + } } \ No newline at end of file diff --git a/Homepage_Scrollbar.css b/Homepage_Scrollbar.css new file mode 100644 index 0000000..3d767d9 --- /dev/null +++ b/Homepage_Scrollbar.css @@ -0,0 +1,29 @@ +/* Atur lebar scrollbar */ +::-webkit-scrollbar { + width: 12px; +} + +/* Background jalanan scrollbar (Track) - Gelap agar neon menyala */ +::-webkit-scrollbar-track { + background: #0b0b15; /* Hitam keunguan sangat gelap */ + border-left: 1px solid #222; +} + +/* Batang Scroll (Thumb) - Efek Neon */ +::-webkit-scrollbar-thumb { + /* Gradasi dari Cyan ke Ungu */ + background: linear-gradient(180deg, #00f2ff, #bc13fe); + border-radius: 10px; + /* Memberikan jarak sedikit agar terlihat melayang */ + border: 3px solid transparent; + background-clip: content-box; +} + +/* Efek saat di-hover (disorot mouse) - Lebih terang */ +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, #5dfaff, #d65eff); + border: 3px solid transparent; + background-clip: content-box; + /* Sedikit shadow untuk efek glow */ + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5); +} \ No newline at end of file diff --git a/Leaderboard.php b/Leaderboard.php index 689876a..dc9463d 100644 --- a/Leaderboard.php +++ b/Leaderboard.php @@ -1,20 +1,23 @@ "error", "leaderboard" => [], "user_rank" => null ]; -// --------------------------------------------------------- -// 1. Ambil Top 10 Global (DIPERBAIKI) -// Ditambahkan "user_id ASC" agar urutannya PASTI (Konsisten) -// Jika skor sama, user dengan ID lebih kecil (daftar duluan) akan di atas -// --------------------------------------------------------- -$query = "SELECT username, score FROM leaderboard ORDER BY score DESC, user_id ASC LIMIT 10"; +// Ambil Top 10 Leaderboard Global +// Urut score terbesar +$query = " + SELECT username, score + FROM leaderboard + ORDER BY score DESC, user_id ASC + LIMIT 10 +"; $result = $conn->query($query); if ($result && $result->num_rows > 0) { @@ -23,14 +26,16 @@ if ($result && $result->num_rows > 0) { } } -// --------------------------------------------------------- -// 2. Ambil Ranking User Login (DIPERBAIKI) -// --------------------------------------------------------- +// Ambil Ranking User yang Sedang Login (your rank) if (isset($_SESSION['user_id'])) { $my_id = $_SESSION['user_id']; - - // Ambil score user saat ini - $scoreQuery = $conn->prepare("SELECT username, score FROM leaderboard WHERE user_id = ?"); + + // Ambil username & score user + $scoreQuery = $conn->prepare(" + SELECT username, score + FROM leaderboard + WHERE user_id = ? + "); $scoreQuery->bind_param("i", $my_id); $scoreQuery->execute(); $scoreResult = $scoreQuery->get_result(); @@ -39,38 +44,33 @@ if (isset($_SESSION['user_id'])) { $myScore = $scoreRow['score']; $myUsername = $scoreRow['username']; - // --- LOGIKA BARU --- - // Hitung ranking dengan Tie-Breaker. - // Rank = Jumlah orang yang skornya LEBIH BESAR - // DITAMBAH Jumlah orang yang skornya SAMA tapi ID-nya LEBIH KECIL (dia di atas kita) - + // Hitung jumlah user yang berada di atas $rankQuery = $conn->prepare(" - SELECT COUNT(*) as rank_above - FROM leaderboard - WHERE score > ? - OR (score = ? AND user_id < ?) + SELECT COUNT(*) AS rank_above + FROM leaderboard + WHERE score > ? + OR (score = ? AND user_id < ?) "); - - // Kita bind 3 parameter: score, score, id $rankQuery->bind_param("iii", $myScore, $myScore, $my_id); - $rankQuery->execute(); $rankResult = $rankQuery->get_result(); $rankRow = $rankResult->fetch_assoc(); - - // Rank adalah jumlah orang di atas kita + 1 - $myRank = $rankRow['rank_above'] + 1; + // Rank = jumlah di atas + 1 $response['user_rank'] = [ "username" => $myUsername, "score" => $myScore, - "rank" => $myRank + "rank" => $rankRow['rank_above'] + 1 ]; } } +// Status sukses $response['status'] = "success"; + +// Kirim JSON ke client echo json_encode($response); +// Tutup koneksi database $conn->close(); ?> \ No newline at end of file diff --git a/Login.php b/Login.php index b1a0dc9..63f4a75 100644 --- a/Login.php +++ b/Login.php @@ -1,57 +1,70 @@ prepare("SELECT id, password FROM users WHERE username = ?"); +// Cek Username di Database +$stmt = $conn->prepare( + "SELECT id, password FROM users WHERE username = ?" +); $stmt->bind_param("s", $username); -$stmt->execute(); -$stmt->store_result(); +$stmt->execute(); +$stmt->store_result(); +// Jika Username Tidak Ada if ($stmt->num_rows === 0) { - echo json_encode(["success" => false, "message" => "Username Not Found"]); + echo json_encode([ + "success" => false, + "message" => "Username Not Found" + ]); $stmt->close(); $conn->close(); exit; } -// 🔴 PERBAIKAN 2: Bind result untuk menangkap 'id' dan 'password' -$stmt->bind_result($userId, $hashedPassword); +// Ambil Data User +$stmt->bind_result($userId, $hashedPassword); // Ambil id & password hash $stmt->fetch(); +// Cek Password if (password_verify($password, $hashedPassword)) { - // 🔴 PERBAIKAN 3: Simpan 'user_id' ke dalam SESSION + + // Simpan data login ke session $_SESSION['user_id'] = $userId; $_SESSION['username'] = $username; - + + // Kirim respon login sukses echo json_encode([ - "success" => true, + "success" => true, "message" => "Login successful", "username" => $username, - "token" => bin2hex(random_bytes(32)) + "token" => bin2hex(random_bytes(32)) // Token acak (bukan JWT) ]); + } else { - echo json_encode(["success" => false, "message" => "Incorrect password"]); + + // Password salah + echo json_encode([ + "success" => false, + "message" => "Incorrect password" + ]); } -$stmt->close(); -$conn->close(); -?> \ No newline at end of file +$stmt->close(); // Tutup statement +$conn->close(); // Tutup koneksi DB +?> diff --git a/Logout.js b/Logout.js index d5611cc..f3a0997 100644 --- a/Logout.js +++ b/Logout.js @@ -7,16 +7,13 @@ const loginBtn = document.querySelector('.btn-login'); if (loggedInUser && loginBtn) { - // User is logged in - change to Logout button + // Mengubah button login menjadi logout loginBtn.textContent = 'Logout'; loginBtn.classList.remove('btn-login'); loginBtn.classList.add('btn-logout'); - loginBtn.href = '#'; // Prevent navigation - - // Remove existing click listeners (untuk menghindari duplikat) + loginBtn.href = '#'; loginBtn.replaceWith(loginBtn.cloneNode(true)); - // Get new reference dan add logout handler const newLogoutBtn = document.querySelector('.btn-logout'); if (newLogoutBtn) { newLogoutBtn.addEventListener('click', handleLogoutClick); @@ -28,31 +25,30 @@ } } - // ==================== LOGOUT CLICK HANDLER ==================== + // LOGOUT CLICK HANDLER + // Mencegah halaman refresh secara otomatis dan langsung menampilkan jendela pop-up konfirmasi logout function handleLogoutClick(e) { e.preventDefault(); showLogoutModal(); } - // ==================== SHOW LOGOUT CONFIRMATION MODAL ==================== + // SHOW LOGOUT CONFIRMATION MODAL function showLogoutModal() { const overlay = document.getElementById('logout-overlay'); if (overlay) { overlay.style.display = 'flex'; setTimeout(() => overlay.classList.add('active'), 10); - - // Setup modal buttons setupModalButtons(); } } - // ==================== SETUP MODAL BUTTONS ==================== + // SETUP MODAL BUTTONS function setupModalButtons() { const cancelBtn = document.getElementById('btn-logout-cancel'); const confirmBtn = document.getElementById('btn-logout-confirm'); const overlay = document.getElementById('logout-overlay'); - // Remove previous listeners + // Remove previous listeners (mencegah error double click) if (cancelBtn) { const newCancelBtn = cancelBtn.cloneNode(true); cancelBtn.replaceWith(newCancelBtn); @@ -65,7 +61,7 @@ newConfirmBtn.onclick = confirmLogout; } - // Close on overlay click + // Close on overlay click (area gelap) if (overlay) { overlay.onclick = function(e) { if (e.target === overlay) { @@ -74,11 +70,12 @@ }; } - // ESC key to close (gunakan named function agar bisa di-remove) + // Aktifkan tombol escape untuk menutup modal document.addEventListener('keydown', handleEscapeKey); } - // ==================== HANDLE ESCAPE KEY ==================== + // HANDLE ESCAPE KEY + // Menutup jendela pop-up (modal) logout menggunakan tombol keyboard Esc function handleEscapeKey(e) { if (e.key === 'Escape') { const overlay = document.getElementById('logout-overlay'); @@ -88,7 +85,7 @@ } } - // ==================== CLOSE LOGOUT MODAL ==================== + // CLOSE LOGOUT MODAL function closeLogoutModal() { const overlay = document.getElementById('logout-overlay'); if (overlay) { @@ -103,12 +100,10 @@ } // CONFIRM LOGOUT - // GANTI fungsi confirmLogout() yang lama dengan ini: - function confirmLogout() { console.log("Mencoba logout ke server..."); // Debugging - // 1. Panggil PHP untuk hancurkan sesi server + // Panggil PHP untuk hancurkan sesi server fetch('Logout.php') .then(response => { console.log("Respon server:", response); @@ -117,12 +112,12 @@ .then(data => { console.log("Status Logout:", data); - // 2. Hapus data di browser (Session Storage) + // Hapus data di browser (Session Storage) sessionStorage.removeItem("loggedInUser"); - sessionStorage.removeItem("authToken"); // Kalau ada + sessionStorage.removeItem("authToken"); sessionStorage.clear(); // Bersihkan semuanya biar aman - // 3. Tutup Modal & Redirect + // Tutup Modal & Redirect closeLogoutModal(); showSuccessModal(); @@ -139,7 +134,7 @@ }); } - // ==================== SHOW SUCCESS MODAL ==================== + // SHOW SUCCESS MODAL function showSuccessModal() { const successOverlay = document.getElementById('logout-success-overlay'); if (successOverlay) { @@ -157,7 +152,8 @@ } - // ==================== INITIALIZE ==================== + // INITIALIZE + // Menjamin agar kode tidak mencuri start sebelum elemen HTML tersedia. function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkLoginStatus); @@ -166,8 +162,6 @@ } } - - // Start init(); })(); \ No newline at end of file diff --git a/Logout.php b/Logout.php index 33601c8..b7179fd 100644 --- a/Logout.php +++ b/Logout.php @@ -1,10 +1,11 @@ "success", "message" => "Logout berhasil" diff --git a/Register.php b/Register.php index 880a18f..b5df56b 100644 --- a/Register.php +++ b/Register.php @@ -1,98 +1,85 @@ "error", - "message" => "Username and password are required" - ]); + echo json_encode(["status" => "error", "message" => "Username and password are required"]); exit; } -// ✅ Validasi panjang password +// Validasi Password if (strlen($password) < 6) { - echo json_encode([ - "status" => "error", - "message" => "Password must be at least 6 characters" - ]); + echo json_encode(["status" => "error", "message" => "Password must be at least 6 characters"]); exit; } -// ✅ Validasi format username +// Validasi Username if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) { - echo json_encode([ - "status" => "error", - "message" => "Username may only contain letters, numbers, and underscores (3–20 characters)" - ]); + echo json_encode(["status" => "error", "message" => "Invalid username format"]); exit; } -// ✅ Cek apakah username sudah ada +// Cek Username $check = $conn->prepare("SELECT id FROM users WHERE username = ?"); $check->bind_param("s", $username); $check->execute(); $check->store_result(); if ($check->num_rows > 0) { - echo json_encode([ - "status" => "error", - "message" => "Username is already taken" - ]); + echo json_encode(["status" => "error", "message" => "Username already taken"]); $check->close(); $conn->close(); exit; } $check->close(); -// ✅ Hash password dan insert ke database -$hashedPassword = password_hash($password, PASSWORD_DEFAULT); -$created_at = date("Y-m-d H:i:s"); +// Simpan User Baru +$hashedPassword = password_hash($password, PASSWORD_DEFAULT); // Hash password +$created_at = date("Y-m-d H:i:s"); // Waktu daftar -$stmt = $conn->prepare("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)"); +$stmt = $conn->prepare( + "INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)" +); $stmt->bind_param("sss", $username, $hashedPassword, $created_at); if ($stmt->execute()) { - // 🔥 PERBAIKAN UTAMA DI SINI (AUTO-LOGIN) 🔥 $new_user_id = $stmt->insert_id; // Ambil ID user baru - - session_start(); - $_SESSION['user_id'] = $new_user_id; // Set Session ID - $_SESSION['username'] = $username; // Set Session Username + + session_start(); // Mulai session + $_SESSION['user_id'] = $new_user_id; // Set session ID + $_SESSION['username'] = $username; // Set session username echo json_encode([ - "status" => "success", + "status" => "success", "message" => "Registration successful", "registered_at" => $created_at ]); } else { echo json_encode([ - "status" => "error", - "message" => "Failed to register: " . $conn->error + "status" => "error", + "message" => "Registration failed" ]); } -$stmt->close(); -$conn->close(); +$stmt->close(); // Tutup statement +$conn->close(); // Tutup DB ?> \ No newline at end of file diff --git a/Score.php b/Score.php index efc7ac0..d32d57c 100644 --- a/Score.php +++ b/Score.php @@ -1,74 +1,99 @@ "error", "message" => "Not logged in or session is invalid"]); + echo json_encode([ + "status" => "error", + "message" => "Not logged in" + ]); exit; } -$username = $_SESSION['username']; -$user_id = $_SESSION['user_id']; // AMBIL ID DARI SESSION -$score = intval($_POST['score'] ?? 0); +// Ambil Data +$username = $_SESSION['username']; // Username dari session +$user_id = $_SESSION['user_id']; // User ID dari session +$score = intval($_POST['score'] ?? 0); // Score dari client -// Validasi score +// Validasi Score if ($score <= 0) { - echo json_encode(["status" => "error", "message" => "Invalid score"]); + echo json_encode([ + "status" => "error", + "message" => "Invalid score" + ]); exit; } -// Cek apakah user sudah punya record di leaderboard -$checkStmt = $conn->prepare("SELECT score FROM leaderboard WHERE user_id = ?"); -$checkStmt->bind_param("i", $user_id); // Cek berdasarkan ID, lebih akurat daripada username +// Cek Data Leaderboard +$checkStmt = $conn->prepare( + "SELECT score FROM leaderboard WHERE user_id = ?" +); // Cek berdasarkan user_id +$checkStmt->bind_param("i", $user_id); $checkStmt->execute(); $result = $checkStmt->get_result(); +// Jika sudah ada score if ($result->num_rows > 0) { $row = $result->fetch_assoc(); $oldScore = $row['score']; - + if ($score > $oldScore) { - // Update score berdasarkan user_id - $updateStmt = $conn->prepare("UPDATE leaderboard SET score = ?, username = ? WHERE user_id = ?"); - // Kita update username juga untuk jaga-jaga kalau user ganti nama + + // Update jika score lebih tinggi + $updateStmt = $conn->prepare( + "UPDATE leaderboard SET score = ?, username = ? WHERE user_id = ?" + ); $updateStmt->bind_param("isi", $score, $username, $user_id); - + if ($updateStmt->execute()) { echo json_encode([ - "status" => "success", - "message" => "High Score baru tercatat!", + "status" => "success", + "message" => "New high score saved", "newHighScore" => true ]); } else { - echo json_encode(["status" => "error", "message" => "Failed to update the database"]); + echo json_encode([ + "status" => "error", + "message" => "Failed to update score" + ]); } $updateStmt->close(); + } else { + + // Score lebih rendah dari record lama echo json_encode([ - "status" => "success", - "message" => "The score is lower than the previous record", + "status" => "success", + "message" => "Score not higher than previous", "newHighScore" => false ]); } + } else { - // Masukkan user_id, username, dan score - $insertStmt = $conn->prepare("INSERT INTO leaderboard (user_id, username, score) VALUES (?, ?, ?)"); + + // Jika Belum Ada Score + $insertStmt = $conn->prepare( + "INSERT INTO leaderboard (user_id, username, score) VALUES (?, ?, ?)" + ); $insertStmt->bind_param("isi", $user_id, $username, $score); - + if ($insertStmt->execute()) { echo json_encode([ - "status" => "success", - "message" => "The first score has been successfully saved", + "status" => "success", + "message" => "First score saved", "newHighScore" => true ]); } else { - echo json_encode(["status" => "error", "message" => "Failed to insert into database"]); + echo json_encode([ + "status" => "error", + "message" => "Failed to save score" + ]); } $insertStmt->close(); } -$checkStmt->close(); -$conn->close(); +$checkStmt->close(); +$conn->close(); ?> \ No newline at end of file diff --git a/Score_Request.js b/Score_Request.js index f39328e..191409e 100644 --- a/Score_Request.js +++ b/Score_Request.js @@ -1,13 +1,15 @@ function saveScore(score) { + // Kirim score ke server fetch('Score.php', { - method: 'POST', + method: 'POST', // Method POST headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded' // Data dikirim dalam format form data }, - body: 'score=' + encodeURIComponent(score) + body: 'score=' + encodeURIComponent(score) // Data score yang dikirim }) - .then(response => response.json()) + .then(response => response.json()) // Ubah response ke JSON .then(data => { + // Cek status dari server if (data.status === "success") { console.log("Score saved successfully"); } else {