This commit is contained in:
Evelyn Sucitro 2025-12-15 23:56:52 +07:00
parent 08f00ee6d6
commit 05623f58df
18 changed files with 845 additions and 791 deletions

601
2048.css
View File

@ -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;
@ -838,208 +442,3 @@ h1 {
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;
}

View File

@ -4,12 +4,14 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>2048</title>
<link rel="stylesheet" href="2048.css"/>
<link rel="stylesheet" href="2048_Header.css"/>
<link rel="stylesheet" href="2048_Background_Effects.css"/>
<link rel="stylesheet" href="2048_Floating_Particles.css"/>
<link rel="stylesheet" href="2048.css"/>
<link rel="stylesheet" href="2048_Tiles_Colors.css"/>
<link rel="stylesheet" href="2048_Sound.css"/>
<link rel="stylesheet" href="2048_Button.css"/>
<link rel="stylesheet" href="2048_Sound.css"/>
<link rel="stylesheet" href="2048_Modal.css"/>
<link rel="stylesheet" href="2048_Responsive.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>

View File

@ -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;

71
2048_Header.css Normal file
View File

@ -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;
}

288
2048_Modal.css Normal file
View File

@ -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;
}

View File

@ -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);

View File

@ -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`);
});
}
});

View File

@ -1,7 +1,11 @@
<?php
// Mengizinkan akses dari semua domain
header('Access-Control-Allow-Origin: *');
// Menentukan metode HTTP yang diizinkan (POST, GET, OPTIONS)
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
// Menentukan header yang diizinkan dalam request
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Mengatur tipe konten output menjadi format JSON
header('Content-Type: application/json');
$DB_HOST = "202.46.28.160";
@ -10,6 +14,7 @@ $DB_USER = "evelyn";
$DB_PASS = "evelynsc25";
$DB_NAME = "web";
// Memeriksa apakah ada error saat menghubungkan ke database
$conn = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME, $DB_PORT);
if ($conn->connect_error) {
http_response_code(500);

View File

@ -6,6 +6,7 @@
<title>Homepage</title>
<link rel="stylesheet" href="Homepage.css">
<link rel="stylesheet" href="Homepage_Credit.css">
<link rel="stylesheet" href="Homepage_Scrollbar.css">
<link rel="stylesheet" href="Homepage_Responsive.css">
</head>
<body>

View File

@ -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 */
}
}

29
Homepage_Scrollbar.css Normal file
View File

@ -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);
}

View File

@ -1,20 +1,23 @@
<?php
header('Content-Type: application/json');
require 'Connection.php';
session_start();
header('Content-Type: application/json'); // Response berupa JSON
require 'Connection.php'; // Koneksi database
session_start(); // Ambil session user login
// Response default
$response = [
"status" => "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
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();
?>

View File

@ -1,5 +1,4 @@
<?php
// ... (Header CORS tetap sama) ...
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
@ -11,47 +10,61 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit();
}
session_start();
include 'Connection.php';
session_start(); // Mulai session (login state)
include 'Connection.php'; // Koneksi database
$input = json_decode(file_get_contents('php://input'), true);
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
// Ambil Data Login dari Client
$input = json_decode(file_get_contents('php://input'), true); // Ambil body JSON
$username = $input['username'] ?? ''; // Username dari client
$password = $input['password'] ?? ''; // Password dari client
// ... (Validasi input kosong tetap sama) ...
// 🔴 PERBAIKAN 1: Tambahkan 'id' di dalam SELECT
$stmt = $conn->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();
// 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,
"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();
$stmt->close(); // Tutup statement
$conn->close(); // Tutup koneksi DB
?>

View File

@ -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();
})();

View File

@ -1,10 +1,11 @@
<?php
session_start();
header('Content-Type: application/json');
header('Content-Type: application/json'); // Response berupa JSON
session_unset();
session_destroy();
session_unset(); // Hapus semua data session
session_destroy(); // Hancurkan session login
// Kirim respon logout sukses
echo json_encode([
"status" => "success",
"message" => "Logout berhasil"

View File

@ -1,85 +1,72 @@
<?php
// ✅ Set timezone Indonesia (WIB)
date_default_timezone_set('Asia/Jakarta');
date_default_timezone_set('Asia/Jakarta'); // Set waktu WIB
// ✅ CORS Headers
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
header('Content-Type: application/json');
// ✅ Handle preflight OPTIONS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// Koneksi Database
include 'Connection.php';
// ✅ Handle input
// Ambil Input
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? $_POST['username'] ?? '');
$password = $input['password'] ?? $_POST['password'] ?? '';
$username = trim($input['username'] ?? $_POST['username'] ?? ''); // Username
$password = $input['password'] ?? $_POST['password'] ?? ''; // Password
// ✅ Validasi input kosong
// Validasi Kosong
if (empty($username) || empty($password)) {
echo json_encode([
"status" => "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 (320 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",
@ -89,10 +76,10 @@ if ($stmt->execute()) {
} else {
echo json_encode([
"status" => "error",
"message" => "Failed to register: " . $conn->error
"message" => "Registration failed"
]);
}
$stmt->close();
$conn->close();
$stmt->close(); // Tutup statement
$conn->close(); // Tutup DB
?>

View File

@ -1,70 +1,95 @@
<?php
session_start();
session_start(); // Mulai session
header('Content-Type: application/json');
require 'Connection.php'; // Gunakan require agar stop jika file tidak ada
require 'Connection.php'; // Koneksi DB
// 1. Pastikan user login & user_id tersedia
// Cek Login
if (!isset($_SESSION['username']) || !isset($_SESSION['user_id'])) {
echo json_encode(["status" => "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!",
"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",
"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",
"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();
}

View File

@ -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 {