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 {