Merge branch 'main' of https://git-eng.ukwms.ac.id/2526-web/kelompok06-2048
This commit is contained in:
commit
cd0dccc729
8
2048.css
8
2048.css
@ -1047,14 +1047,14 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Best Score Display - ORANGE gradient */
|
/* High Score Display - ORANGE gradient */
|
||||||
.best-score-display {
|
.high-score-display {
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
padding-top: 28px;
|
padding-top: 28px;
|
||||||
border-top: 2px solid rgba(255, 140, 0, 0.25);
|
border-top: 2px solid rgba(255, 140, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.best-score-label {
|
.high-score-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: rgba(255, 160, 50, 0.7);
|
color: rgba(255, 160, 50, 0.7);
|
||||||
@ -1063,7 +1063,7 @@ h1 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.best-score-value {
|
.high-score-value {
|
||||||
font-size: clamp(34px, 5vw, 42px);
|
font-size: clamp(34px, 5vw, 42px);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
background: linear-gradient(135deg, #ff8c00 0%, #ffa500 50%, #ffb347 100%);
|
background: linear-gradient(135deg, #ff8c00 0%, #ffa500 50%, #ffb347 100%);
|
||||||
|
|||||||
12
2048.html
12
2048.html
@ -210,7 +210,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="score-box">
|
<div class="score-box">
|
||||||
<div class="score-label">HIGH SCORE</div>
|
<div class="score-label">HIGH SCORE</div>
|
||||||
<div class="score-value" id="best-score">0</div>
|
<div class="score-value" id="high-score">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -325,14 +325,14 @@
|
|||||||
New High Score
|
New High Score
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Best Score Display - show if NOT new record -->
|
<!-- high Score Display - show if NOT new record -->
|
||||||
<div
|
<div
|
||||||
class="best-score-display"
|
class="high-score-display"
|
||||||
id="best-score-display"
|
id="high-score-display"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
>
|
>
|
||||||
<div class="best-score-label">High Score</div>
|
<div class="high-score-label">High Score</div>
|
||||||
<div class="best-score-value" id="modal-best-score">0</div>
|
<div class="high-score-value" id="modal-high-score">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
46
2048.js
46
2048.js
@ -1,11 +1,18 @@
|
|||||||
|
|
||||||
|
|
||||||
/* ------------------------
|
/* ------------------------
|
||||||
State & Variables
|
State & Variables
|
||||||
------------------------ */
|
------------------------ */
|
||||||
let board = [];
|
let board = [];
|
||||||
let currentScore = 0;
|
let currentScore = 0;
|
||||||
let bestScore = parseInt(localStorage.getItem('bestScore2048')) || 0;
|
|
||||||
|
// 1. Ambil username dari sessionStorage (sesuai sistem login kamu)
|
||||||
|
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
||||||
|
|
||||||
|
// 2. Buat nama kunci unik, misal: "highScore2048_budi"
|
||||||
|
const storageKey = 'highScore2048_' + currentUser;
|
||||||
|
|
||||||
|
// 3. Ambil skor milik user tersebut saja
|
||||||
|
|
||||||
|
let highScore = parseInt(localStorage.getItem(storageKey)) || 0;
|
||||||
let lastMoveDir = null;
|
let lastMoveDir = null;
|
||||||
let isMoving = false;
|
let isMoving = false;
|
||||||
let mergesInCurrentMove = 0;
|
let mergesInCurrentMove = 0;
|
||||||
@ -71,7 +78,7 @@ function checkAndShowTutorial() {
|
|||||||
DOM Ready
|
DOM Ready
|
||||||
------------------------ */
|
------------------------ */
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
updateBestScoreDisplay();
|
updateHighScoreDisplay();
|
||||||
setupBoard();
|
setupBoard();
|
||||||
addNewTile();
|
addNewTile();
|
||||||
addNewTile();
|
addNewTile();
|
||||||
@ -223,17 +230,18 @@ function updateScoreDisplay() {
|
|||||||
scoreEl.textContent = currentScore;
|
scoreEl.textContent = currentScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentScore > bestScore) {
|
if (currentScore > highScore) {
|
||||||
bestScore = currentScore;
|
highScore = currentScore;
|
||||||
localStorage.setItem('bestScore2048', bestScore);
|
// Gunakan storageKey yang sudah kita buat di atas (dinamis sesuai user)
|
||||||
updateBestScoreDisplay();
|
localStorage.setItem(storageKey, highScore);
|
||||||
}
|
updateHighScoreDisplay();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateBestScoreDisplay() {
|
function updateHighScoreDisplay() {
|
||||||
const bestScoreEl = document.getElementById('best-score');
|
const highScoreEl = document.getElementById('high-score');
|
||||||
if (bestScoreEl) {
|
if (highScoreEl) {
|
||||||
bestScoreEl.textContent = bestScore;
|
highScoreEl.textContent = highScore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,7 +609,7 @@ function showGameOver() {
|
|||||||
console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload.");
|
console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNewHighScore = finalScore >= bestScore && finalScore > 0;
|
const isNewHighScore = finalScore >= highScore && finalScore > 0;
|
||||||
|
|
||||||
const finalScoreEl = document.getElementById('final-score');
|
const finalScoreEl = document.getElementById('final-score');
|
||||||
if (finalScoreEl) {
|
if (finalScoreEl) {
|
||||||
@ -609,16 +617,16 @@ function showGameOver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newHighScoreBadge = document.getElementById('new-high-score-badge');
|
const newHighScoreBadge = document.getElementById('new-high-score-badge');
|
||||||
const bestScoreDisplay = document.getElementById('best-score-display');
|
const highScoreDisplay = document.getElementById('high-score-display');
|
||||||
|
|
||||||
if (isNewHighScore) {
|
if (isNewHighScore) {
|
||||||
if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block';
|
if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block';
|
||||||
if (bestScoreDisplay) bestScoreDisplay.style.display = 'none';
|
if (highScoreDisplay) highScoreDisplay.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
if (newHighScoreBadge) newHighScoreBadge.style.display = 'none';
|
if (newHighScoreBadge) newHighScoreBadge.style.display = 'none';
|
||||||
if (bestScoreDisplay) bestScoreDisplay.style.display = 'block';
|
if (highScoreDisplay) highScoreDisplay.style.display = 'block';
|
||||||
const modalBestScore = document.getElementById('modal-best-score');
|
const modalHighScore = document.getElementById('modal-high-score');
|
||||||
if (modalBestScore) modalBestScore.textContent = bestScore;
|
if (modalHighScore) modalHighScore.textContent = highScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameOverOverlay = document.getElementById('game-over-overlay');
|
const gameOverOverlay = document.getElementById('game-over-overlay');
|
||||||
|
|||||||
233
Leaderboard.css
233
Leaderboard.css
@ -198,7 +198,9 @@ h1::before {
|
|||||||
box-shadow: 0 0 15px rgba(0, 234, 255, 0.8);
|
box-shadow: 0 0 15px rgba(0, 234, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Leaderboard Item */
|
/* --- LEADERBOARD ITEMS & COLORS (HARMONISASI) --- */
|
||||||
|
|
||||||
|
/* Base Item Style */
|
||||||
.leaderboard-item {
|
.leaderboard-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -206,7 +208,7 @@ h1::before {
|
|||||||
padding: 18px 25px;
|
padding: 18px 25px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: rgba(30, 0, 50, 0.5);
|
background: rgba(30, 0, 50, 0.5);
|
||||||
border: 1px solid rgba(0, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -217,57 +219,105 @@ h1::before {
|
|||||||
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rank Styles */
|
/* === RANK 1: CYAN NEON (Juara) === */
|
||||||
.leaderboard-item.rank-1 {
|
.leaderboard-item.rank-1,
|
||||||
background: linear-gradient(135deg, rgba(0, 234, 255, 0.25), rgba(0, 255, 136, 0.2));
|
.your-rank-container.rank-1 .your-rank-item {
|
||||||
border: 2px solid rgba(0, 234, 255, 0.6);
|
background: linear-gradient(90deg, rgba(0, 234, 255, 0.25) 0%, rgba(0, 234, 255, 0.05) 100%);
|
||||||
box-shadow:
|
border: 2px solid #00eaff;
|
||||||
0 0 25px rgba(0, 234, 255, 0.4),
|
box-shadow: 0 0 15px rgba(0, 234, 255, 0.3);
|
||||||
inset 0 0 25px rgba(0, 234, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-item.rank-2 {
|
|
||||||
background: linear-gradient(135deg, rgba(255, 0, 255, 0.2), rgba(204, 0, 255, 0.15));
|
|
||||||
border: 2px solid rgba(255, 0, 255, 0.5);
|
|
||||||
box-shadow: 0 0 18px rgba(255, 0, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-item.rank-3 {
|
|
||||||
background: linear-gradient(135deg, rgba(138, 43, 226, 0.25), rgba(75, 0, 130, 0.2));
|
|
||||||
border: 2px solid rgba(138, 43, 226, 0.5);
|
|
||||||
box-shadow: 0 0 15px rgba(138, 43, 226, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rank Badge */
|
|
||||||
.rank-badge {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: bold;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-1 .rank-badge {
|
.rank-1 .rank-badge {
|
||||||
background: linear-gradient(135deg, #00eaff, #00ff88);
|
background: #00eaff;
|
||||||
color: #0c001a;
|
color: #000;
|
||||||
box-shadow: 0 0 20px rgba(0, 234, 255, 0.8);
|
box-shadow: 0 0 15px #00eaff;
|
||||||
animation: pulseBadge 2s ease-in-out infinite;
|
animation: pulseBadge 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-2 .rank-badge {
|
.rank-1 .player-name {
|
||||||
background: linear-gradient(135deg, #ff00ff, #cc00ff);
|
/* 1. Matikan efek gradient aneh pada teks */
|
||||||
|
background: none;
|
||||||
|
-webkit-text-fill-color: initial;
|
||||||
|
background-clip: border-box;
|
||||||
|
|
||||||
|
/* 2. Samakan ukuran font dengan yang lain (sebelumnya 22px jadi 20px) */
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
/* 3. Warna Putih Solid + Sedikit Glow Cyan biar tetap spesial tapi rapi */
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
box-shadow: 0 0 18px rgba(255, 0, 255, 0.7);
|
text-shadow: 0 0 10px rgba(0, 234, 255, 0.8);
|
||||||
|
|
||||||
|
/* Hapus filter drop-shadow berlebih */
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIX: Warna Score Rank 1 jadi Putih Solid (Anti Silau) */
|
||||||
|
.rank-1 .score-value {
|
||||||
|
/* 1. Ukuran disamakan dengan yang lain */
|
||||||
|
font-size: 22px !important;
|
||||||
|
|
||||||
|
/* 2. Warna Teks Putih Solid */
|
||||||
|
background: none !important;
|
||||||
|
-webkit-text-fill-color: initial !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
|
||||||
|
/* 3. GLOWING: Ganti bayangan hitam jadi cahaya Cyan */
|
||||||
|
text-shadow: 0 0 10px #00eaff, 0 0 20px rgba(0, 234, 255, 0.4) !important;
|
||||||
|
|
||||||
|
/* Reset filter */
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === RANK 2: MAGENTA NEON (Runner Up) === */
|
||||||
|
.leaderboard-item.rank-2,
|
||||||
|
.your-rank-container.rank-2 .your-rank-item {
|
||||||
|
background: linear-gradient(90deg, rgba(255, 0, 255, 0.25) 0%, rgba(255, 0, 255, 0.05) 100%);
|
||||||
|
border: 2px solid #ff00ff;
|
||||||
|
box-shadow: 0 0 15px rgba(255, 0, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-2 .rank-badge {
|
||||||
|
background: #ff00ff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 15px #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-2 .score-value {
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 10px #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-2 .player-score {
|
||||||
|
color: #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === RANK 3: VIOLET NEON (Third) === */
|
||||||
|
.leaderboard-item.rank-3,
|
||||||
|
.your-rank-container.rank-3 .your-rank-item {
|
||||||
|
background: linear-gradient(90deg, rgba(138, 43, 226, 0.25) 0%, rgba(138, 43, 226, 0.05) 100%);
|
||||||
|
border: 2px solid #8a2be2;
|
||||||
|
box-shadow: 0 0 15px rgba(138, 43, 226, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-3 .rank-badge {
|
.rank-3 .rank-badge {
|
||||||
background: linear-gradient(135deg, #8a2be2, #4b0082);
|
background: #8a2be2;
|
||||||
color: #ffffff;
|
color: #fff;
|
||||||
box-shadow: 0 0 15px rgba(138, 43, 226, 0.6);
|
box-shadow: 0 0 15px #8a2be2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-3 .score-value {
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 10px #8a2be2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-3 .player-score {
|
||||||
|
color: #a855f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === RANK OTHER === */
|
||||||
|
.leaderboard-item.rank-other {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-other .rank-badge {
|
.rank-other .rank-badge {
|
||||||
@ -276,6 +326,7 @@ h1::before {
|
|||||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
@keyframes pulseBadge {
|
@keyframes pulseBadge {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
@ -287,7 +338,7 @@ h1::before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Player Info */
|
/* Player Info Common */
|
||||||
.player-info {
|
.player-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -305,22 +356,11 @@ h1::before {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-1 .player-name {
|
/* Score Common */
|
||||||
background: linear-gradient(90deg, #00eaff, #00ff88);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: none;
|
|
||||||
filter: drop-shadow(0 0 6px rgba(0, 234, 255, 0.5));
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Score */
|
|
||||||
.player-score {
|
.player-score {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #00eaff;
|
color: #00eaff;
|
||||||
text-shadow: 0 0 12px rgba(0, 234, 255, 0.7);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
@ -332,25 +372,6 @@ h1::before {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-1 .score-value {
|
|
||||||
font-size: 25px;
|
|
||||||
background: linear-gradient(90deg, #00eaff, #ff00ff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
filter: drop-shadow(0 0 10px rgba(0, 234, 255, 0.8));
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-2 .player-score {
|
|
||||||
color: #ff00ff;
|
|
||||||
text-shadow: 0 0 12px rgba(255, 0, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-3 .player-score {
|
|
||||||
color: #a855f7;
|
|
||||||
text-shadow: 0 0 12px rgba(138, 43, 226, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-label {
|
.score-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -358,15 +379,26 @@ h1::before {
|
|||||||
color: rgba(200, 200, 255, 0.5);
|
color: rgba(200, 200, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Your Rank Fixed Section */
|
/* Badge Size Common */
|
||||||
|
.rank-badge {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Your Rank Section Structure */
|
||||||
.your-rank-container {
|
.your-rank-container {
|
||||||
background: rgba(20, 0, 40, 0.9);
|
background: transparent;
|
||||||
border-radius: 14px;
|
border: none;
|
||||||
padding: 15px 25px;
|
box-shadow: none;
|
||||||
border: 2px solid rgba(0, 255, 136, 0.6);
|
padding: 0;
|
||||||
box-shadow:
|
margin-top: 20px;
|
||||||
0 0 25px rgba(0, 255, 136, 0.4),
|
|
||||||
inset 0 0 20px rgba(0, 255, 136, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.your-rank-item {
|
.your-rank-item {
|
||||||
@ -375,15 +407,34 @@ h1::before {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 18px 25px;
|
padding: 18px 25px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.15), rgba(0, 234, 255, 0.1));
|
/* Default style if not top 3 */
|
||||||
|
background: linear-gradient(135deg, rgba(0, 255, 136, 0.15), rgba(0, 0, 0, 0.5));
|
||||||
border: 2px solid rgba(0, 255, 136, 0.5);
|
border: 2px solid rgba(0, 255, 136, 0.5);
|
||||||
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* --- RESPONSIVE FIX (MODE LAPTOP/LAYAR KECIL) --- */
|
||||||
|
|
||||||
|
@media screen and (max-width: 1440px) {
|
||||||
|
/* Mencegah container nabrak tombol back */
|
||||||
|
.container {
|
||||||
|
width: 88%;
|
||||||
|
padding: 30px 50px;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 35px 30px;
|
padding: 35px 30px;
|
||||||
|
width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -394,18 +445,6 @@ h1::before {
|
|||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-back {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
<ul class="leaderboard-list" id="leaderboardList"></ul>
|
<ul class="leaderboard-list" id="leaderboardList"></ul>
|
||||||
|
|
||||||
|
<div id="userRankContainer"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="Leaderboard.js"></script>
|
<script src="Leaderboard.js"></script>
|
||||||
|
|||||||
@ -7,12 +7,53 @@ function loadLeaderboard() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === "success") {
|
if (data.status === "success") {
|
||||||
|
// Render Top 10
|
||||||
renderLeaderboard(data.leaderboard);
|
renderLeaderboard(data.leaderboard);
|
||||||
|
|
||||||
|
// Render Ranking Saya (User Rank)
|
||||||
|
if (data.user_rank) {
|
||||||
|
renderUserRank(data.user_rank);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error loading leaderboard:", error));
|
.catch(error => console.error("Error loading leaderboard:", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... fungsi renderLeaderboard tetap sama ...
|
||||||
|
|
||||||
|
// TAMBAHKAN FUNGSI INI DI BAWAH
|
||||||
|
function renderUserRank(user) {
|
||||||
|
const container = document.getElementById('userRankContainer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Format angka skor
|
||||||
|
const formattedScore = new Intl.NumberFormat().format(user.score);
|
||||||
|
|
||||||
|
// HTML structure sesuai CSS .your-rank-container
|
||||||
|
const html = `
|
||||||
|
<div class="your-rank-container">
|
||||||
|
<div style="color: #00ff88; margin-bottom: 10px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px;">
|
||||||
|
</div>
|
||||||
|
<div class="your-rank-item">
|
||||||
|
<div class="rank-badge" style="background: #00ff88; color: #000; box-shadow: 0 0 15px #00ff88;">
|
||||||
|
${user.rank}
|
||||||
|
</div>
|
||||||
|
<div class="player-info">
|
||||||
|
<div class="player-name">${escapeHtml(user.username)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="player-score">
|
||||||
|
<div class="score-value">${formattedScore}</div>
|
||||||
|
<div class="score-label">Points</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... fungsi escapeHtml tetap sama ...
|
||||||
|
|
||||||
function renderLeaderboard(players) {
|
function renderLeaderboard(players) {
|
||||||
const listContainer = document.getElementById('leaderboardList');
|
const listContainer = document.getElementById('leaderboardList');
|
||||||
if (!listContainer) return; // Safety check
|
if (!listContainer) return; // Safety check
|
||||||
|
|||||||
@ -1,23 +1,76 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require 'Connection.php';
|
require 'Connection.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
$query = "SELECT username, score FROM leaderboard ORDER BY score DESC LIMIT 10";
|
$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";
|
||||||
$result = $conn->query($query);
|
$result = $conn->query($query);
|
||||||
|
|
||||||
$leaderboard = [];
|
|
||||||
|
|
||||||
if ($result && $result->num_rows > 0) {
|
if ($result && $result->num_rows > 0) {
|
||||||
while ($row = $result->fetch_assoc()) {
|
while ($row = $result->fetch_assoc()) {
|
||||||
$leaderboard[] = $row;
|
$response['leaderboard'][] = $row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kirim data ke frontend dalam bentuk JSON
|
// ---------------------------------------------------------
|
||||||
echo json_encode([
|
// 2. Ambil Ranking User Login (DIPERBAIKI)
|
||||||
"status" => "success",
|
// ---------------------------------------------------------
|
||||||
"leaderboard" => $leaderboard
|
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 = ?");
|
||||||
|
$scoreQuery->bind_param("i", $my_id);
|
||||||
|
$scoreQuery->execute();
|
||||||
|
$scoreResult = $scoreQuery->get_result();
|
||||||
|
|
||||||
|
if ($scoreRow = $scoreResult->fetch_assoc()) {
|
||||||
|
$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)
|
||||||
|
|
||||||
|
$rankQuery = $conn->prepare("
|
||||||
|
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;
|
||||||
|
|
||||||
|
$response['user_rank'] = [
|
||||||
|
"username" => $myUsername,
|
||||||
|
"score" => $myScore,
|
||||||
|
"rank" => $myRank
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['status'] = "success";
|
||||||
|
echo json_encode($response);
|
||||||
|
|
||||||
$conn->close();
|
$conn->close();
|
||||||
?>
|
?>
|
||||||
18
Login.php
18
Login.php
@ -1,12 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
// ✅ CORS Headers HARUS di paling atas sebelum apapun
|
// ... (Header CORS tetap sama) ...
|
||||||
header('Access-Control-Allow-Origin: *');
|
header('Access-Control-Allow-Origin: *');
|
||||||
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||||
header('Access-Control-Max-Age: 86400');
|
header('Access-Control-Max-Age: 86400');
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// ✅ Handle preflight OPTIONS request
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
exit();
|
exit();
|
||||||
@ -15,17 +14,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
session_start();
|
session_start();
|
||||||
include 'Connection.php';
|
include 'Connection.php';
|
||||||
|
|
||||||
// Ambil data dari JSON body
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
$username = $input['username'] ?? '';
|
$username = $input['username'] ?? '';
|
||||||
$password = $input['password'] ?? '';
|
$password = $input['password'] ?? '';
|
||||||
|
|
||||||
if (empty($username) || empty($password)) {
|
// ... (Validasi input kosong tetap sama) ...
|
||||||
echo json_encode(["success" => false, "message" => "Username dan password wajib diisi"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $conn->prepare("SELECT password FROM users WHERE username = ?");
|
// 🔴 PERBAIKAN 1: Tambahkan 'id' di dalam SELECT
|
||||||
|
$stmt = $conn->prepare("SELECT id, password FROM users WHERE username = ?");
|
||||||
$stmt->bind_param("s", $username);
|
$stmt->bind_param("s", $username);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$stmt->store_result();
|
$stmt->store_result();
|
||||||
@ -37,11 +33,15 @@ if ($stmt->num_rows === 0) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->bind_result($hashedPassword);
|
// 🔴 PERBAIKAN 2: Bind result untuk menangkap 'id' dan 'password'
|
||||||
|
$stmt->bind_result($userId, $hashedPassword);
|
||||||
$stmt->fetch();
|
$stmt->fetch();
|
||||||
|
|
||||||
if (password_verify($password, $hashedPassword)) {
|
if (password_verify($password, $hashedPassword)) {
|
||||||
|
// 🔴 PERBAIKAN 3: Simpan 'user_id' ke dalam SESSION
|
||||||
|
$_SESSION['user_id'] = $userId;
|
||||||
$_SESSION['username'] = $username;
|
$_SESSION['username'] = $username;
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"message" => "Login berhasil",
|
"message" => "Login berhasil",
|
||||||
|
|||||||
@ -62,6 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="Score_Request.js"></script>
|
||||||
<script type="module" src="Register.js"></script>
|
<script type="module" src="Register.js"></script>
|
||||||
<script src="Animation_Register.js"></script>
|
<script src="Animation_Register.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
31
Register.js
31
Register.js
@ -28,17 +28,34 @@ document.getElementById("registerForm").addEventListener("submit", async functio
|
|||||||
|
|
||||||
// Button loading state
|
// Button loading state
|
||||||
const submitBtn = this.querySelector("button[type='submit']");
|
const submitBtn = this.querySelector("button[type='submit']");
|
||||||
const originalText = submitBtn.textContent;
|
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await registerRequest(username, password);
|
const data = await registerRequest(username, password);
|
||||||
|
|
||||||
// API temenmu return: { status: "success", message: "..." }
|
|
||||||
if (data.status === "success") {
|
if (data.status === "success") {
|
||||||
|
// 1. Tampilkan modal sukses
|
||||||
showModal("success", "Register Successful!", data.message);
|
showModal("success", "Register Successful!", data.message);
|
||||||
|
|
||||||
|
// 🔥 PERBAIKAN: SAVE SCORE & REDIRECT 🔥
|
||||||
|
|
||||||
|
// Coba simpan skor jika variable 'score' atau 'gameScore' tersedia di global scope
|
||||||
|
// Atau jika kamu menyimpan skor sementara di localStorage
|
||||||
|
const pendingScore = localStorage.getItem('lastScore'); // Contoh jika pakai localStorage
|
||||||
|
|
||||||
|
if (pendingScore && typeof saveScore === "function") {
|
||||||
|
console.log("Menyimpan skor pending: " + pendingScore);
|
||||||
|
saveScore(pendingScore);
|
||||||
|
} else if (typeof score !== 'undefined') {
|
||||||
|
// Jika variabel global 'score' ada (dari file game logic)
|
||||||
|
saveScore(score);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect ke halaman utama setelah 1.5 detik
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = "index.html"; // Ubah sesuai halaman game/menu kamu
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
showModal("error", "Register Failed!", data.message || "An error occurred.");
|
showModal("error", "Register Failed!", data.message || "An error occurred.");
|
||||||
setInputError("username");
|
setInputError("username");
|
||||||
@ -48,11 +65,9 @@ document.getElementById("registerForm").addEventListener("submit", async functio
|
|||||||
console.error("Register Error:", error);
|
console.error("Register Error:", error);
|
||||||
|
|
||||||
let msg = "A connection error occurred.";
|
let msg = "A connection error occurred.";
|
||||||
|
|
||||||
if (error.message === "Failed to fetch") {
|
if (error.message === "Failed to fetch") {
|
||||||
msg = "Unable to connect to server.";
|
msg = "Unable to connect to server.";
|
||||||
}
|
}
|
||||||
|
|
||||||
showModal("error", "Error!", msg);
|
showModal("error", "Error!", msg);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@ -68,8 +83,10 @@ document.getElementById("confirmPassword").addEventListener("input", clearInputS
|
|||||||
// Efek error merah neon
|
// Efek error merah neon
|
||||||
function setInputError(id) {
|
function setInputError(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
el.style.borderColor = "#ff0080";
|
if(el) {
|
||||||
el.style.boxShadow = "0 0 20px rgba(255, 0, 128, 0.3)";
|
el.style.borderColor = "#ff0080";
|
||||||
|
el.style.boxShadow = "0 0 20px rgba(255, 0, 128, 0.3)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilangkan error visual
|
// Hilangkan error visual
|
||||||
|
|||||||
24
Register.php
24
Register.php
@ -2,12 +2,12 @@
|
|||||||
// ✅ Set timezone Indonesia (WIB)
|
// ✅ Set timezone Indonesia (WIB)
|
||||||
date_default_timezone_set('Asia/Jakarta');
|
date_default_timezone_set('Asia/Jakarta');
|
||||||
|
|
||||||
// ✅ CORS Headers - di paling atas
|
// ✅ CORS Headers
|
||||||
header('Access-Control-Allow-Origin: *');
|
header('Access-Control-Allow-Origin: *');
|
||||||
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||||
header('Access-Control-Max-Age: 86400');
|
header('Access-Control-Max-Age: 86400');
|
||||||
header('Content-Type: application/json'); // Cukup 1x saja
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// ✅ Handle preflight OPTIONS
|
// ✅ Handle preflight OPTIONS
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
@ -17,7 +17,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
|
|
||||||
include 'Connection.php';
|
include 'Connection.php';
|
||||||
|
|
||||||
// ✅ Handle input dari JSON body atau POST form
|
// ✅ Handle input
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
$username = trim($input['username'] ?? $_POST['username'] ?? '');
|
$username = trim($input['username'] ?? $_POST['username'] ?? '');
|
||||||
$password = $input['password'] ?? $_POST['password'] ?? '';
|
$password = $input['password'] ?? $_POST['password'] ?? '';
|
||||||
@ -31,7 +31,7 @@ if (empty($username) || empty($password)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Validasi panjang password minimal
|
// ✅ Validasi panjang password
|
||||||
if (strlen($password) < 6) {
|
if (strlen($password) < 6) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"status" => "error",
|
"status" => "error",
|
||||||
@ -68,15 +68,23 @@ $check->close();
|
|||||||
|
|
||||||
// ✅ Hash password dan insert ke database
|
// ✅ Hash password dan insert ke database
|
||||||
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$created_at = date("Y-m-d H:i:s");
|
||||||
|
|
||||||
$stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
|
$stmt = $conn->prepare("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)");
|
||||||
$stmt->bind_param("ss", $username, $hashedPassword);
|
$stmt->bind_param("sss", $username, $hashedPassword, $created_at);
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
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
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"status" => "success",
|
"status" => "success",
|
||||||
"message" => "Pendaftaran berhasil",
|
"message" => "Pendaftaran berhasil & Auto-login",
|
||||||
"registered_at" => date("Y-m-d H:i:s") // timestamp sudah WIB
|
"registered_at" => $created_at
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user