-
How to Play
-
-
🖥️ PC Controls
@@ -255,8 +223,6 @@
-
-
📱 Mobile Controls
@@ -274,8 +240,6 @@
-
-
-
No More Moves!
Game Over
-
Your Score
0
-
🏆 New High Score!
-
-
-
diff --git a/2048.js b/2048.js
index 151e578..4e437e9 100644
--- a/2048.js
+++ b/2048.js
@@ -1,42 +1,30 @@
-/* ------------------------
- 1. GAME STATE & VARIABLES
- ------------------------ */
let board = [];
let currentScore = 0;
-// Ambil username dari sessionStorage (Cek apakah ada user login)
const loggedInUser = sessionStorage.getItem("loggedInUser");
-// Tentukan user saat ini (jika tidak ada login, pakai "guest")
const currentUser = loggedInUser || "guest";
-// Buat nama kunci unik untuk penyimpanan
const storageKey = 'highScore2048_' + currentUser;
-// --- PERBAIKAN DI SINI ---
let highScore = 0;
if (loggedInUser) {
- // JIKA SUDAH LOGIN: Ambil High Score dari memori localStorage
highScore = parseInt(localStorage.getItem(storageKey)) || 0;
} else {
- // JIKA BELUM LOGIN (GUEST): Selalu mulai High Score dari 0 saat refresh
highScore = 0;
}
-// -------------------------
let lastMoveDir = null;
let isMoving = false;
let mergesInCurrentMove = 0;
-// Sound State (baca dari localStorage atau default ON)
let soundState = {
bg: localStorage.getItem('sound_bg') !== 'false',
pop: localStorage.getItem('sound_pop') !== 'false',
merge: localStorage.getItem('sound_merge') !== 'false'
};
-// Volume State (0-100 for each sound)
let volumeState = {
music: parseInt(localStorage.getItem('vol_music')) || 25,
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
diff --git a/2048_Audio.js b/2048_Audio.js
index 39ac26e..f683687 100644
--- a/2048_Audio.js
+++ b/2048_Audio.js
@@ -1,43 +1,21 @@
-/* ==========================================
- 2048 AUDIO - SOUND MANAGEMENT SYSTEM
- ==========================================
- fitur:
- 1. 3 Audio objects (bg, pop, merge)
- 2. Volume control terpisah untuk setiap audio
- 3. Icon dinamis sesuai level volume
- 4. Panel volume dengan inputLock integration
- ========================================== */
-
-/* ==========================================
- AUDIO OBJECTS
- ========================================== */
const audio = {
- bg: new Audio("Background_Music.mp3"), // Background music
- pop: new Audio("Pop.mp3"), // Sound saat tile spawn
- merge: new Audio("Merge.mp3") // Sound saat tile merge
+ bg: new Audio("Background_Music.mp3"),
+ pop: new Audio("Pop.mp3"),
+ merge: new Audio("Merge.mp3")
};
-audio.bg.loop = true; // Background music loop terus
+audio.bg.loop = true;
-/* ==========================================
- UPDATE VOLUME DARI STATE & SLIDERS
- ========================================== */
function updateAudioVolumes() {
- // Formula: volume = enabled ? (slider / 100) : 0
audio.bg.volume = soundState.bg ? (volumeState.music / 100) : 0;
audio.pop.volume = soundState.pop ? (volumeState.pop / 100) : 0;
audio.merge.volume = soundState.merge ? (volumeState.merge / 100) : 0;
}
-/* ==========================================
- PLAY BACKGROUND MUSIC (dengan unlock)
- ========================================== */
function tryPlayBg() {
if (!soundState.bg || volumeState.music === 0) return;
- // Coba play, kalau di-block browser (autoplay policy)
audio.bg.play().catch(() => {
- // Setup unlock: tunggu user interaction
const unlock = () => {
if (soundState.bg && volumeState.music > 0) audio.bg.play().catch(()=>{});
window.removeEventListener("keydown", unlock);
@@ -48,24 +26,17 @@ function tryPlayBg() {
});
}
-/* ==========================================
- PLAY SOUND (dengan mute check)
- ========================================== */
function playSound(soundObj) {
try {
- // Guard: cek mute state sebelum play
if (soundObj === audio.pop && (!soundState.pop || volumeState.pop === 0)) return;
if (soundObj === audio.merge && (!soundState.merge || volumeState.merge === 0)) return;
if (soundObj === audio.bg && (!soundState.bg || volumeState.music === 0)) return;
- soundObj.currentTime = 0; // Restart dari awal
- soundObj.play().catch(() => {}); // Play with error handling
+ soundObj.currentTime = 0;
+ soundObj.play().catch(() => {});
} catch (e) {}
}
-/* ==========================================
- INIT VOLUME SLIDERS - Setup Event Listeners
- ========================================== */
function initVolumeControl() {
updateAudioVolumes();
@@ -73,33 +44,28 @@ function initVolumeControl() {
const popSlider = document.getElementById('vol-pop');
const mergeSlider = document.getElementById('vol-merge');
- /* --- MUSIC SLIDER --- */
if (musicSlider) {
- // Load nilai dari localStorage
musicSlider.value = volumeState.music;
updateSliderFill(musicSlider, volumeState.music);
document.getElementById('vol-music-display').textContent = volumeState.music + '%';
- // Event listener saat slider digeser
musicSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
- volumeState.music = val; // Update state
- audio.bg.volume = val / 100; // Set volume langsung
- localStorage.setItem('vol_music', val); // Save ke browser
+ volumeState.music = val;
+ audio.bg.volume = val / 100;
+ localStorage.setItem('vol_music', val);
document.getElementById('vol-music-display').textContent = val + '%';
- updateSliderFill(e.target, val); // Update visual fill
- updateMainSoundIcon(); // Update icon
+ updateSliderFill(e.target, val);
+ updateMainSoundIcon();
- // Auto play/pause background music
if (val > 0 && audio.bg.paused && soundState.bg) {
- tryPlayBg(); // Play kalau volume > 0
+ tryPlayBg();
} else if (val === 0) {
- audio.bg.pause(); // Pause kalau volume = 0 (muted)
+ audio.bg.pause();
}
});
}
- /* --- POP SLIDER (sama seperti music) --- */
if (popSlider) {
popSlider.value = volumeState.pop;
updateSliderFill(popSlider, volumeState.pop);
@@ -116,7 +82,6 @@ function initVolumeControl() {
});
}
- /* --- MERGE SLIDER (sama seperti music) --- */
if (mergeSlider) {
mergeSlider.value = volumeState.merge;
updateSliderFill(mergeSlider, volumeState.merge);
@@ -134,110 +99,83 @@ function initVolumeControl() {
}
updateMainSoundIcon();
- setupVolumePanelEvents(); // Setup panel interactions
+ setupVolumePanelEvents();
}
-/* ==========================================
- SETUP PANEL EVENTS - Open/Close Logic
- ==========================================
- Ini yang nge-link dengan inputLocked di Controls
- ========================================== */
function setupVolumePanelEvents() {
const btnSoundMain = document.getElementById('btn-sound-main');
const volumePanel = document.getElementById('volume-panel');
const volumeBackdrop = document.getElementById('volume-backdrop');
if (btnSoundMain && volumePanel) {
- // Event: Klik tombol sound main
btnSoundMain.addEventListener('click', (e) => {
- e.stopPropagation(); // Jangan trigger event di parent
+ e.stopPropagation();
const isActive = volumePanel.classList.contains('active');
if (isActive) {
- // TUTUP PANEL
volumePanel.classList.remove('active');
if (volumeBackdrop) volumeBackdrop.classList.remove('active');
- inputLocked = false; // UNLOCK - game input aktif lagi
+ inputLocked = false;
} else {
- // BUKA PANEL
volumePanel.classList.add('active');
if (volumeBackdrop) volumeBackdrop.classList.add('active');
- inputLocked = true; // LOCK - blokir swipe & keyboard
+ inputLocked = true;
}
});
- // Event: Klik backdrop (tutup panel)
volumeBackdrop.addEventListener('click', () => {
volumePanel.classList.remove('active');
volumeBackdrop.classList.remove('active');
- inputLocked = false; // UNLOCK
+ inputLocked = false;
});
- // Event: Klik di luar panel (desktop)
document.addEventListener('click', (e) => {
if (!volumePanel.contains(e.target) &&
!btnSoundMain.contains(e.target) &&
(!volumeBackdrop || !volumeBackdrop.contains(e.target))) {
volumePanel.classList.remove('active');
if (volumeBackdrop) volumeBackdrop.classList.remove('active');
- inputLocked = false; // UNLOCK
+ inputLocked = false;
}
});
- // Event: Klik di dalam panel (jangan tutup)
volumePanel.addEventListener('click', (e) => {
- e.stopPropagation(); // Prevent close
+ e.stopPropagation();
});
}
}
-/* ==========================================
- UPDATE VISUAL FILL SLIDER
- ========================================== */
function updateSliderFill(slider, value) {
- // Set CSS custom property untuk animasi fill
- // Dipakai di CSS: background: linear-gradient(...)
slider.style.setProperty('--value', value + '%');
}
-/* ==========================================
- UPDATE ICON DINAMIS - Sesuai Volume Level
- ========================================== */
function updateMainSoundIcon() {
const btnMain = document.getElementById('btn-sound-main');
if (!btnMain) return;
- // Ambil semua icon
const iconFull = btnMain.querySelector('.sound-full');
const iconMedium = btnMain.querySelector('.sound-medium');
const iconLow = btnMain.querySelector('.sound-low');
const iconMuted = btnMain.querySelector('.sound-muted');
- // Hitung rata-rata volume dari 3 slider
const totalVolume = volumeState.music + volumeState.pop + volumeState.merge;
const avgVolume = totalVolume / 3;
- // Hide semua icon dulu
if (iconFull) iconFull.style.display = 'none';
if (iconMedium) iconMedium.style.display = 'none';
if (iconLow) iconLow.style.display = 'none';
if (iconMuted) iconMuted.style.display = 'none';
- // Show icon yang sesuai dengan level volume
if (totalVolume === 0) {
- // Semua muted
if (iconMuted) iconMuted.style.display = 'block';
btnMain.classList.add('all-muted');
} else {
btnMain.classList.remove('all-muted');
if (avgVolume >= 60) {
- // Volume tinggi (≥60%)
if (iconFull) iconFull.style.display = 'block';
} else if (avgVolume >= 30) {
- // Volume sedang (30-59%)
if (iconMedium) iconMedium.style.display = 'block';
} else {
- // Volume rendah (1-29%)
if (iconLow) iconLow.style.display = 'block';
}
}
diff --git a/2048_Controls.js b/2048_Controls.js
index 77a647f..23ba965 100644
--- a/2048_Controls.js
+++ b/2048_Controls.js
@@ -1,30 +1,13 @@
-/* ==========================================
- 2048 CONTROLS
- ==========================================
- fungsi utama:
- 1. handleKey() - mendeteksi keyboard (Arrow/WASD)
- 2. Touch Events - meneteksi swipe mobile
- 3. inputLocked - prevent konflik saat panel volume buka
- ========================================== */
-
-// flag untuk blokir input game saat setting panel aktif
let inputLocked = false;
-/* ==========================================
- KEYBOARD INPUT HANDLER
- ========================================== */
function handleKey(e) {
- // Blokir input kalau:
- // 1. Tile sedang bergerak (isMoving = true)
- // 2. Panel volume sedang dibuka (inputLocked = true)
if (isMoving || inputLocked) return;
let moved = false;
const k = e.key;
- // Deteksi 4 arah: Arrow keys atau WASD
if (k === "ArrowLeft" || k === "a" || k === "A") {
- e.preventDefault(); // Cegah scroll halaman
+ e.preventDefault();
moved = moveLeft(); }
else if (k === "ArrowRight" || k === "d" || k === "D") {
e.preventDefault();
@@ -36,23 +19,19 @@ function handleKey(e) {
e.preventDefault();
moved = moveDown(); }
- // Kalau tile berhasil bergerak
if (moved) {
- isMoving = true; // Lock input biar nggak spam
+ isMoving = true;
- // Delay 100ms untuk animasi selesai
setTimeout(() => {
- const added = addNewTile(); // Spawn tile baru
+ const added = addNewTile();
- // Cek game over: board penuh ATAU nggak bisa move
if (!added || !canMove()) {
setTimeout(() => showGameOver(), 300);
}
- isMoving = false; // Unlock input
+ isMoving = false;
}, 100);
} else {
- //Invalid move - Kasih feedback shake
const b = document.getElementById("board");
if (b) {
b.classList.add("shake");
@@ -61,49 +40,36 @@ function handleKey(e) {
}
}
-/* ==========================================
- TOUCH/SWIPE INPUT HANDLER (Mobile)
- ========================================== */
-
-// Simpan posisi saat awal sentuhan
let touchStartX = 0;
let touchStartY = 0;
-// Event 1: Catat posisi awal saat jari menyentuh layar
document.addEventListener("touchstart", function (e) {
- if (inputLocked) return; // Jangan catat swipe kalau panel volume aktif
+ if (inputLocked) return;
const t = e.touches[0];
- touchStartX = t.clientX; // Posisi X awal
- touchStartY = t.clientY; // Posisi Y awal
-}, { passive: true }); // Passive = performa lebih smooth
+ touchStartX = t.clientX;
+ touchStartY = t.clientY;
+}, { passive: true });
-// Event 2: Deteksi arah swipe saat jari diangkat
document.addEventListener("touchend", function (e) {
- if (isMoving || inputLocked) return; // cek isMoving DAN inputLocked
+ if (isMoving || inputLocked) return;
const t = e.changedTouches[0];
- // Hitung selisih posisi (delta)
- const dx = t.clientX - touchStartX; //Horizontal movement
- const dy = t.clientY - touchStartY; //Vertical movement
+ const dx = t.clientX - touchStartX;
+ const dy = t.clientY - touchStartY;
let moved = false;
- // Tentukan arah swipe berdasarkan delta terbesar
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
- // Swipe HORIZONTAL (kiri/kanan)
- // Minimal 30px biar nggak terlalu sensitif
- if (dx > 0) moved = moveRight(); // swipe kanan
- else moved = moveLeft(); // swipe kiri
+ if (dx > 0) moved = moveRight();
+ else moved = moveLeft();
} else if (Math.abs(dy) > 30) {
- // Swipe VERTICAL (atas/bawah)
- if (dy > 0) moved = moveDown(); // swipe bawah
- else moved = moveUp(); // swipe atas
+ if (dy > 0) moved = moveDown();
+ else moved = moveUp();
}
- // Logic sama seperti keyboard handler
if (moved) {
isMoving = true;
setTimeout(() => {
diff --git a/2048_Floating_Particles.js b/2048_Floating_Particles.js
index 1a520fb..23939f4 100644
--- a/2048_Floating_Particles.js
+++ b/2048_Floating_Particles.js
@@ -1,75 +1,41 @@
-/* ==========================================
- 2048 FLOATING PARTICLES - BACKGROUND DECORATION
- ==========================================
- fungsi
- - createParticle() - Buat partikel yang naik dari bawah
- - Self-recycling system (partikel hilang → buat baru)
- - Efek drift horizontal untuk gerakan natural
- ========================================== */
-
-// Floating Particles System - Particles rising from bottom
(function() {
- // Ambil container untuk partikel
const container = document.getElementById('floating-particles');
- if (!container) return; // Guard: kalau container nggak ada, skip
+ if (!container) return;
- // Config warna partikel (5 warna neon)
const particleColors = ['cyan', 'pink', 'purple', 'green', 'orange'];
- const particleCount = 25; // Total partikel yang muncul
+ const particleCount = 25;
- /* ==========================================
- CREATE PARTICLE - Buat 1 partikel
- ========================================== */
function createParticle() {
- // Buat element div untuk partikel
const particle = document.createElement('div');
- // Pilih warna random dari array
const randomColor = particleColors[Math.floor(Math.random() * particleColors.length)];
particle.className = `floating-particle ${randomColor}`;
- // POSISI HORIZONTAL RANDOM (0-100%)
const leftPos = Math.random() * 100;
particle.style.left = leftPos + '%';
- // DRIFT HORIZONTAL (gerakan ke kiri/kanan saat naik)
- // Range: -75px sampai +75px
const drift = (Math.random() - 0.5) * 150;
particle.style.setProperty('--drift', drift + 'px');
- // DURASI ANIMASI RANDOM (8-18 detik)
- // Semakin lama = semakin smooth & dramatis
const duration = 8 + Math.random() * 10;
particle.style.animationDuration = duration + 's';
- // DELAY RANDOM (0-5 detik)
- // Biar nggak semua partikel muncul bareng (staggered)
const delay = Math.random() * 5;
particle.style.animationDelay = delay + 's';
- // UKURAN RANDOM (6-14px)
const size = 6 + Math.random() * 8;
particle.style.width = size + 'px';
particle.style.height = size + 'px';
- // Append ke container
container.appendChild(particle);
- // ♻️ SELF-RECYCLING SYSTEM
- // Setelah animasi selesai → hapus & buat baru
setTimeout(() => {
- particle.remove(); // Hapus partikel lama
- createParticle(); // Buat partikel baru (infinite loop)
- }, (duration + delay) * 1000); // Total waktu = duration + delay
+ particle.remove();
+ createParticle();
+ }, (duration + delay) * 1000);
}
- /* ==========================================
- INITIALIZE - Buat semua partikel awal
- ========================================== */
- // Loop buat 25 partikel
for (let i = 0; i < particleCount; i++) {
- // Stagger creation: jeda 200ms per partikel
- // Biar nggak semua muncul sekaligus (smooth)
setTimeout(() => createParticle(), i * 200);
}
-})(); // IIFE (Immediately Invoked Function Expression) - langsung jalan
\ No newline at end of file
+})();
\ No newline at end of file
diff --git a/2048_Logic.js b/2048_Logic.js
index 708be7b..8cd8c71 100644
--- a/2048_Logic.js
+++ b/2048_Logic.js
@@ -1,19 +1,4 @@
-/* ==========================================
- 2048 LOGIC
- ==========================================
- fungsi inti:
- 1. addNewTile() - spawn tile baru (angka 2)
- 2. slide() - algoritma merge tile
- 3. move functions - 4 arah pergerakan
- 4. canMove() - Cek game over
- ========================================== */
-
-/* ==========================================
- SPAWN TILE BARU
- ========================================== */
-
function addNewTile() {
- // Step 1: Cari semua cell kosong (value = 0)
const empty = [];
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
@@ -21,74 +6,54 @@ function addNewTile() {
}
}
- // Kalau board penuh, return false (game over)
if (empty.length === 0) return false;
- // Step 2: Pilih 1 posisi random dari cell kosong
const spot = empty[Math.floor(Math.random() * empty.length)];
- board[spot.r][spot.c] = 2; // Tile baru selalu angka 2
+ board[spot.r][spot.c] = 2;
- // Step 3: Animasi "new" + sound effect
const tile = document.getElementById(`${spot.r}-${spot.c}`);
if (tile) {
- tile.classList.add("new"); // Trigger animasi pop
- playSound(audio.pop); // Sound effect
+ tile.classList.add("new");
+ playSound(audio.pop);
setTimeout(() => tile.classList.remove("new"), 300);
}
- updateTile(spot.r, spot.c, 2); // Update visual tile
+ updateTile(spot.r, spot.c, 2);
return true;
}
-/* ==========================================
- HELPER: FILTER ANGKA NOL
- ========================================== */
function filterZero(row) {
- // Buang semua 0 dari array
- // Contoh: [2, 0, 2, 0] → [2, 2]
return row.filter(n => n !== 0);
}
-/* ==========================================
- ALGORITMA SLIDE & MERGE (CORE!)
- ==========================================
- Ini yang bikin 2+2=4, 4+4=8, dst
- ========================================== */
function slide(row) {
- // Step 1: Filter zero - kumpulkan tile
- // [2, 0, 2, 4] → [2, 2, 4]
row = filterZero(row);
let mergedThisMove = false;
- let mergedPositions = []; // Posisi tile yang di-merge (untuk animasi)
- let mergeCount = 0; // Hitung berapa kali merge (untuk combo)
+ let mergedPositions = [];
+ let mergeCount = 0;
- // Step 2: Loop cek tile sebelahan
for (let i = 0; i < row.length - 1; i++) {
if (row[i] === row[i + 1]) {
- // MERGE! Tile sama ketemu
- row[i] = row[i] * 2; // Tile pertama jadi 2x lipat
+ row[i] = row[i] * 2;
- playSound(audio.merge); // Sound effect merge
+ playSound(audio.merge);
- // Haptic feedback (getaran) di mobile
if (navigator.vibrate) {
navigator.vibrate([80, 20, 80]);
}
- currentScore += row[i]; // Tambah score
- row[i + 1] = 0; // Tile kedua hilang (jadi 0)
+ currentScore += row[i];
+ row[i + 1] = 0;
mergedThisMove = true;
- mergedPositions.push(i); // Simpan posisi untuk animasi
+ mergedPositions.push(i);
mergeCount++;
}
}
-
- // Step 3: Filter zero lagi & padding
- // [4, 0, 4] → [4, 4] → [4, 4, 0, 0]
+
row = filterZero(row);
- while (row.length < 4) row.push(0); // Padding 0 di kanan
+ while (row.length < 4) row.push(0);
return {
row,
@@ -97,42 +62,19 @@ function slide(row) {
mergeCount };
}
-/* ==========================================
- HELPER: CEK ARRAY SAMA ATAU TIDAK
- ========================================== */
-function arraysEqual(a, b) {
- // Bandingin setiap elemen
- // Dipakai untuk cek apakah board berubah setelah move
- return a.length === b.length && a.every((v, i) => v === b[i]);
-}
-
-/* ==========================================
- MOVE FUNCTIONS - 4 ARAH PERGERAKAN
- ==========================================
- Semua punya struktur yang sama:
- 1. Loop setiap baris/kolom
- 2. Panggil slide() untuk merge
- 3. Cek perubahan dengan arraysEqual()
- 4. Refresh board kalau ada perubahan
- ========================================== */
-
- /* MOVE LEFT - Geser ke kiri */
function moveLeft() {
let moved = false;
- let mergedCells = []; // Track cell yang di-merge
- mergesInCurrentMove = 0; // Reset combo counter
+ let mergedCells = [];
+ mergesInCurrentMove = 0;
- // Loop setiap baris (horizontal)
for (let r = 0; r < 4; r++) {
const { row: newRow, mergedPositions, mergeCount } = slide(board[r]);
- // Cek apakah row berubah
if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow;
mergesInCurrentMove += mergeCount;
- // Simpan posisi cell yang merge untuk combo effect
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(c => {
mergedCells.push({ r, c });
@@ -140,7 +82,6 @@ function moveLeft() {
}
}
- // Kalau ada pergerakan, update visual + effect
if (moved) {
refreshBoard();
triggerComboEffect(mergedCells, mergesInCurrentMove);
@@ -148,28 +89,24 @@ function moveLeft() {
return moved;
}
-/* MOVE RIGHT - Geser ke kanan */
function moveRight() {
let moved = false;
let mergedCells = [];
mergesInCurrentMove = 0;
- // TRICK: Reverse → slide → reverse lagi
- // Supaya bisa pakai logic slide yang sama
for (let r = 0; r < 4; r++) {
let reversed = [...board[r]].reverse();
const { row: slid, mergedPositions, mergeCount } = slide(reversed);
- let newRow = slid.reverse(); // Balik lagi ke posisi asli
+ let newRow = slid.reverse();
if (!arraysEqual(newRow, board[r])) moved = true;
board[r] = newRow;
mergesInCurrentMove += mergeCount;
- // Convert posisi merge ke koordinat asli (dari kanan)
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(pos => {
- const c = 3 - pos; // Mirror position
+ const c = 3 - pos;
mergedCells.push({ r, c });
});
}
@@ -182,19 +119,15 @@ function moveRight() {
return moved;
}
-/* MOVE UP - Geser ke atas */
function moveUp() {
let moved = false;
let mergedCells = [];
mergesInCurrentMove = 0;
- // Loop setiap kolom (vertical)
for (let c = 0; c < 4; c++) {
- // Ambil kolom vertikal sebagai array
const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
const { row: newCol, mergedPositions, mergeCount } = slide(col);
- // Update board per row
for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true;
board[r][c] = newCol[r];
@@ -216,17 +149,15 @@ function moveUp() {
return moved;
}
-/* MOVE DOWN - Geser ke bawah */
function moveDown() {
let moved = false;
let mergedCells = [];
mergesInCurrentMove = 0;
for (let c = 0; c < 4; c++) {
- //TRICK: Ambil kolom dari bawah ke atas (reversed)
const col = [board[3][c], board[2][c], board[1][c], board[0][c]];
const { row: slid, mergedPositions, mergeCount } = slide(col);
- const newCol = slid.reverse(); // Balik ke urutan normal
+ const newCol = slid.reverse();
for (let r = 0; r < 4; r++) {
if (board[r][c] !== newCol[r]) moved = true;
@@ -235,10 +166,9 @@ function moveDown() {
mergesInCurrentMove += mergeCount;
- // Convert posisi merge ke koordinat asli (dari bawah)
if (mergedPositions && mergedPositions.length > 0) {
mergedPositions.forEach(pos => {
- const r = 3 - pos; // Mirror position
+ const r = 3 - pos;
mergedCells.push({ r, c });
});
}
@@ -251,27 +181,22 @@ function moveDown() {
return moved;
}
-/* ==========================================
- CEK GAME OVER
- ========================================== */
function canMove() {
- // Kondisi 1: Ada cell kosong?
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
- if (board[r][c] === 0) return true; // Masih ada ruang
+ if (board[r][c] === 0) return true;
}
}
- // Kondisi 2: Ada tile sebelahan yang bisa di-merge?
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
const current = board[r][c];
- //cek kanan
+
if (c < 3 && board[r][c + 1] === current) return true;
- //cek bawah
+
if (r < 3 && board[r + 1][c] === current) return true;
}
}
- // Kalau kedua kondisi false → STUCK = Game Over
+
return false;
}
\ No newline at end of file
diff --git a/2048_Main.js b/2048_Main.js
index 3e05a30..710ee94 100644
--- a/2048_Main.js
+++ b/2048_Main.js
@@ -1,6 +1,3 @@
-/* ------------------------
- 7. MAIN INITIALIZATION
- ------------------------ */
function restartGame() {
hideGameOver();
resetScore();
@@ -23,9 +20,7 @@ function goHome() {
window.location.href = "Homepage.html";
}
-/* Event Listeners Setup */
function setupEventListeners() {
- // Tutorial Modal
const btnTutorial = document.getElementById('btn-tutorial');
const tutorialOverlay = document.getElementById('tutorial-overlay');
const closeTutorial = document.getElementById('close-tutorial');
@@ -36,7 +31,6 @@ function setupEventListeners() {
if (e.target === tutorialOverlay) tutorialOverlay.style.display = 'none';
});
- // Restart & Game Over buttons
const btnRestart = document.getElementById('btn-restart');
if (btnRestart) btnRestart.addEventListener('click', restartGame);
@@ -54,7 +48,6 @@ function setupEventListeners() {
if (e.target === this) hideGameOver();
});
- // Sound Buttons (Mute Toggles)
const btnSoundBg = document.getElementById('btn-sound-bg');
const btnSoundPop = document.getElementById('btn-sound-pop');
const btnSoundMerge = document.getElementById('btn-sound-merge');
@@ -64,7 +57,6 @@ function setupEventListeners() {
localStorage.setItem('sound_bg', soundState.bg);
updateAudioVolumes();
if(soundState.bg) tryPlayBg(); else audio.bg.pause();
- // Tambahkan logika update tombol UI jika ada (toggle class)
});
if (btnSoundPop) btnSoundPop.addEventListener('click', () => {
@@ -80,13 +72,12 @@ function setupEventListeners() {
});
}
-/* DOM Ready */
document.addEventListener("DOMContentLoaded", () => {
updateHighScoreDisplay();
setupBoard();
addNewTile();
addNewTile();
- initVolumeControl(); // Starts audio logic
+ initVolumeControl();
tryPlayBg();
document.addEventListener("keydown", handleKey);
setupEventListeners();
diff --git a/2048_Modal.css b/2048_Modal.css
index 54a6f6d..a04d7b9 100644
--- a/2048_Modal.css
+++ b/2048_Modal.css
@@ -215,7 +215,6 @@
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));
@@ -251,7 +250,6 @@
}
}
-/* High Score Display - ORANGE gradient */
.high-score-display {
margin-top: 28px;
padding-top: 28px;
diff --git a/2048_Responsive.css b/2048_Responsive.css
index bfbbce3..3225e18 100644
--- a/2048_Responsive.css
+++ b/2048_Responsive.css
@@ -1,7 +1,4 @@
-
-
@media (max-width: 480px) {
- /* Body & Container */
body {
padding: 8px;
}
@@ -10,7 +7,6 @@
transform: scale(1);
}
- /* Header */
.game-header {
margin-bottom: 16px;
}
@@ -38,7 +34,6 @@
font-size: clamp(20px, 2.8vw, 25px);
}
- /* Top Controls */
.top-controls {
top: clamp(8px, 1.5vh, 14px);
right: clamp(8px, 1.5vw, 14px);
@@ -72,7 +67,6 @@
height: clamp(16px, 2.8vw, 22px);
}
- /* Board */
#board {
max-width: min(90vmin, 100%);
padding: clamp(8px, 1.8vmin, 14px);
@@ -80,18 +74,15 @@
border-radius: 16px;
}
- /* Tiles */
.tile {
font-size: clamp(18px, 3.2vmin, 28px);
border-radius: clamp(6px, 1.5vmin, 12px);
}
- /* Touch Hint */
.touch-hint {
display: block !important;
}
- /* Tutorial Modal */
.tutorial-modal {
padding: 35px 30px;
max-width: 90%;
@@ -111,12 +102,10 @@
margin-bottom: 12px;
}
- /* PC Controls - Hide on Mobile */
.pc-controls {
display: none !important;
}
- /* Mobile Controls - Show on Mobile */
.mobile-controls {
display: block !important;
}
@@ -168,7 +157,6 @@
height: 18px;
}
- /* Game Over Modal */
.game-over-modal {
padding: 40px 28px 35px;
max-width: 90%;
@@ -246,7 +234,6 @@
height: clamp(28px, 5.5vw, 34px);
}
- /* Sound Control */
.sound-control-container {
top: clamp(8px, 1.5vh, 14px);
left: clamp(8px, 1.5vw, 14px);
diff --git a/2048_Sound.css b/2048_Sound.css
index 5f89e7f..b084fcf 100644
--- a/2048_Sound.css
+++ b/2048_Sound.css
@@ -7,7 +7,6 @@
z-index: 100;
}
-/* Sound Button Styling */
.btn-sound {
position: relative;
width: clamp(36px, 6vw, 48px);
@@ -36,7 +35,6 @@
position: absolute;
}
-/* BG Music Button - Purple */
#btn-sound-bg {
background: rgba(50, 0, 70, 0.85);
border-color: rgba(200, 100, 255, 0.45);
@@ -59,7 +57,6 @@
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);
@@ -82,7 +79,6 @@
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);
@@ -105,7 +101,6 @@
color: rgba(255, 170, 0, 1);
}
-/* Hover & Active States */
.btn-sound:hover {
transform: translateY(-2px);
}
@@ -118,7 +113,6 @@
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;
@@ -150,7 +144,6 @@
color: rgba(255, 100, 100, 1) !important;
}
-/* Icon Transitions */
.btn-sound svg.sound-icon,
.btn-sound svg.mute-icon {
transition: all 0.3s ease;
@@ -160,7 +153,6 @@
transform: scale(1.1);
}
-/* FORCE tombol sound untuk selalu visible */
.btn-sound-main {
display: flex !important;
position: relative !important;
@@ -237,7 +229,6 @@
align-items: flex-start;
}
-/* Backdrop Overlay untuk Mobile */
.volume-backdrop {
display: none;
position: fixed;
@@ -252,7 +243,6 @@
display: block;
}
-/* Main Sound Button */
.btn-sound-main {
width: clamp(40px, 6.5vw, 52px);
height: clamp(40px, 6.5vw, 52px);
@@ -300,7 +290,6 @@
transform: translateY(0);
}
-/* Muted State - Red */
.btn-sound-main.all-muted {
background: rgba(60, 0, 10, 0.9) !important;
border-color: rgba(255, 60, 60, 0.7) !important;
@@ -319,7 +308,6 @@
border-color: rgba(255, 80, 80, 0.9) !important;
}
-/* Volume Panel */
.volume-panel {
display: none;
background: linear-gradient(145deg, rgba(20, 0, 40, 0.98), rgba(30, 0, 50, 0.98));
@@ -351,7 +339,6 @@
}
}
-/* Volume Item */
.volume-item {
margin-bottom: 18px;
}
@@ -390,7 +377,6 @@
text-align: right;
}
-/* Volume Slider */
.volume-slider {
width: 100%;
height: 6px;
@@ -453,7 +439,6 @@
transform: scale(1.2);
}
-/* Slider Fill Effect */
.volume-slider {
background: linear-gradient(to right,
rgba(0, 234, 255, 0.3) 0%,
diff --git a/2048_Tutorial_Logic.js b/2048_Tutorial_Logic.js
index a19f040..f839fc4 100644
--- a/2048_Tutorial_Logic.js
+++ b/2048_Tutorial_Logic.js
@@ -1,38 +1,32 @@
function checkAndShowTutorial() {
- // Ambil user yang sedang login (atau guest)
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
const tutorialKey = 'tutorialSeen_' + currentUser;
console.log(`[Tutorial Check] User: ${currentUser}`);
console.log(`[Tutorial Check] Key: ${tutorialKey}`);
- // Cek apakah tutorial sudah pernah dilihat
const hasSeenTutorial = localStorage.getItem(tutorialKey);
console.log(`[Tutorial Check] Status Seen: ${hasSeenTutorial}`);
const tutorialOverlay = document.getElementById('tutorial-overlay');
- // Tampilkan tutorial jika belum pernah dilihat
if (!hasSeenTutorial && tutorialOverlay) {
tutorialOverlay.style.display = 'flex';
}
}
-// Jalankan saat halaman selesai dimuat
document.addEventListener('DOMContentLoaded', () => {
- checkAndShowTutorial(); // Cek & tampilkan tutorial
+ checkAndShowTutorial();
const closeTutorialBtn = document.getElementById('close-tutorial');
const tutorialOverlay = document.getElementById('tutorial-overlay');
- // Event klik tombol tutup tutorial
if (closeTutorialBtn) {
closeTutorialBtn.addEventListener('click', () => {
- // Ambil user aktif saat ini
+
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
const tutorialKey = 'tutorialSeen_' + currentUser;
- // Sembunyikan tutorial & simpan status
if (tutorialOverlay) tutorialOverlay.style.display = 'none';
localStorage.setItem(tutorialKey, 'true');
});
diff --git a/2048_User_Interface.js b/2048_User_Interface.js
index 0507558..5b036c5 100644
--- a/2048_User_Interface.js
+++ b/2048_User_Interface.js
@@ -1,6 +1,3 @@
-/* ------------------------
- 4. UI MANAGER
- ------------------------ */
function setupBoard() {
board = [];
currentScore = 0;
diff --git a/2048_Visual_Effects.js b/2048_Visual_Effects.js
index c0f2af4..4e5a1ad 100644
--- a/2048_Visual_Effects.js
+++ b/2048_Visual_Effects.js
@@ -1,42 +1,17 @@
-/* ==========================================
- 2048 VISUAL EFFECTS - ANIMATION SYSTEM
- ==========================================
- fungsi utama:
- 1. triggerComboEffect() - Efek saat tile merge
- 2. showComboPopup() - Popup combo text (x2, x3, x4+)
- 3. createParticleBurst() - Ledakan partikel dari tile
- 4. createScorePopup() - Score yang terbang ke atas
- 5. getTileColor() - Warna sesuai nilai tile
- ========================================== */
-
-/* ==========================================
- TRIGGER COMBO EFFECT - Main Visual Handler
- ==========================================
- Dipanggil dari move functions di 2048_Logic.js
- ========================================== */
function triggerComboEffect(mergedCells, comboCount) {
- // Guard: kalau nggak ada tile yang merge, skip
if (mergedCells.length === 0) return;
-
- // Loop setiap tile yang di-merge
mergedCells.forEach(cell => {
const tile = document.getElementById(`${cell.r}-${cell.c}`);
+
if (!tile) return;
-
- // Efek 1: Animasi "merge" (scale + glow)
tile.classList.add('merge');
setTimeout(() => tile.classList.remove('merge'), 300);
-
- // Efek 2: Ledakan partikel
createParticleBurst(tile);
-
- // Efek 3: Box shadow glow
tile.style.boxShadow = '0 0 40px currentColor';
setTimeout(() => {
- tile.style.boxShadow = ''; // Reset setelah 300ms
+ tile.style.boxShadow = '';
}, 300);
-
- // Efek 4: Score popup yang terbang ke atas
+
const rect = tile.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
@@ -44,54 +19,44 @@ function triggerComboEffect(mergedCells, comboCount) {
createScorePopup(centerX, centerY, tileValue);
});
- // Efek 5: Combo popup kalau merge ≥2 tile sekaligus
if (comboCount >= 2) {
showComboPopup(comboCount);
}
}
-/* ==========================================
- COMBO POPUP - Text "COMBO x2!", "AMAZING x3!", dst
- ========================================== */
function showComboPopup(comboCount) {
const board = document.getElementById('board');
if (!board) return;
- // Hitung posisi tengah board
const rect = board.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
- // Buat element popup
const popup = document.createElement('div');
popup.className = 'combo-popup';
popup.style.left = centerX + 'px';
popup.style.top = centerY + 'px';
popup.style.position = 'fixed';
popup.style.fontWeight = '900';
- popup.style.pointerEvents = 'none'; // Nggak bisa diklik
- popup.style.zIndex = '9999'; // Di depan semua
- popup.style.transform = 'translate(-50%, -50%)'; // Center alignment
+ popup.style.pointerEvents = 'none';
+ popup.style.zIndex = '9999';
+ popup.style.transform = 'translate(-50%, -50%)';
popup.style.textTransform = 'uppercase';
popup.style.letterSpacing = '3px';
- // Styling berbeda sesuai combo level
if (comboCount === 2) {
- // COMBO x2 - Hijau neon
popup.textContent = 'COMBO x2!';
popup.style.fontSize = '36px';
popup.style.color = '#00ff99';
popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)';
} else if (comboCount === 3) {
- // AMAZING x3 - Pink magenta
popup.textContent = 'AMAZING x3!';
popup.style.fontSize = '42px';
popup.style.color = '#ff00ff';
popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)';
} else if (comboCount >= 4) {
- // PERFECT x4+ - Gold
popup.textContent = 'PERFECT x' + comboCount + '!';
popup.style.fontSize = '48px';
popup.style.color = '#ffd700';
@@ -100,7 +65,6 @@ function showComboPopup(comboCount) {
document.body.appendChild(popup);
- // Animasi: muncul → bounce → hilang
popup.animate([
{ transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', opacity: 0 },
{ transform: 'translate(-50%, -50%) scale(1.3) rotate(5deg)', opacity: 1, offset: 0.3 },
@@ -108,103 +72,85 @@ function showComboPopup(comboCount) {
{ transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)', opacity: 0 }
], {
duration: 1200,
- easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' // Bounce effect
- }).onfinish = () => popup.remove(); // Auto cleanup
+ easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
+ }).onfinish = () => popup.remove();
}
-/* ==========================================
- PARTICLE BURST - Ledakan partikel dari tile
- ========================================== */
function createParticleBurst(tileElement) {
- // Ambil posisi tengah tile
const rect = tileElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
- // Ambil warna tile sesuai nilainya
const tileValue = parseInt(tileElement.textContent);
const tileColor = getTileColor(tileValue);
- // Jumlah partikel random (8-12)
const particleCount = 8 + Math.floor(Math.random() * 5);
- // Buat partikel dalam lingkaran (360°)
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'merge-particle';
particle.style.left = centerX + 'px';
particle.style.top = centerY + 'px';
- particle.style.background = tileColor; // Warna sama dengan tile
+ particle.style.background = tileColor;
document.body.appendChild(particle);
- // Hitung sudut & kecepatan untuk ledakan melingkar
const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
- const velocity = 60 + Math.random() * 40; // 60-100px
- const tx = Math.cos(angle) * velocity; // Posisi X
- const ty = Math.sin(angle) * velocity; // Posisi Y
+ const velocity = 60 + Math.random() * 40;
+ const tx = Math.cos(angle) * velocity;
+ const ty = Math.sin(angle) * velocity;
- // Animasi: meledak keluar sambil mengecil
particle.animate([
{ transform: 'translate(0, 0) scale(1)', opacity: 1 },
{ transform: `translate(${tx}px, ${ty}px) scale(0)`, opacity: 0 }
], {
- duration: 500 + Math.random() * 200, // 500-700ms
- easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' // Smooth ease-out
- }).onfinish = () => particle.remove(); // Auto cleanup
+ duration: 500 + Math.random() * 200,
+ easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
+ }).onfinish = () => particle.remove();
}
}
-/* ==========================================
- SCORE POPUP - Angka score yang terbang ke atas
- ========================================== */
function createScorePopup(x, y, score) {
const popup = document.createElement('div');
popup.className = 'score-popup';
- popup.textContent = '+' + score; // Contoh: "+16", "+32"
+ popup.textContent = '+' + score;
popup.style.left = x + 'px';
popup.style.top = y + 'px';
popup.style.position = 'fixed';
popup.style.fontSize = '24px';
popup.style.fontWeight = '900';
- popup.style.color = '#ffd700'; // Gold
+ popup.style.color = '#ffd700';
popup.style.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)';
popup.style.pointerEvents = 'none';
popup.style.zIndex = '9999';
popup.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(popup);
-
- // Animasi: terbang ke atas sambil fade out
+
popup.animate([
{ transform: 'translate(-50%, -50%) scale(0.5)', opacity: 0 },
{ transform: 'translate(-50%, -70px) scale(1.2)', opacity: 1, offset: 0.3 },
{ transform: 'translate(-50%, -120px) scale(1)', opacity: 0 }
], {
duration: 1000,
- easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' // Bounce ease
+ easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
}).onfinish = () => popup.remove();
}
-/* ==========================================
- GET TILE COLOR - Warna sesuai nilai tile
- ==========================================
- Dipakai untuk partikel biar warnanya match
- ========================================== */
function getTileColor(value) {
const colors = {
- 2: '#00eaff', // Cyan
- 4: '#00ff99', // Green
- 8: '#ff00ff', // Magenta
- 16: '#ff0066', // Pink
- 32: '#ffaa00', // Orange
- 64: '#ff0000', // Red
- 128: '#5f00ff', // Purple
- 256: '#00ffea', // Cyan bright
- 512: '#ff00aa', // Pink bright
- 1024: '#00ffaa', // Green bright
- 2048: '#ffd700' // Gold (winning tile!)
+ 2: '#00eaff',
+ 4: '#00ff99',
+ 8: '#ff00ff',
+ 16: '#ff0066',
+ 32: '#ffaa00',
+ 64: '#ff0000',
+ 128: '#5f00ff',
+ 256: '#00ffea',
+ 512: '#ff00aa',
+ 1024: '#00ffaa',
+ 2048: '#ffd700'
};
- // Return warna sesuai value, default cyan kalau nggak ada
+
return colors[value] || '#00eaff';
}
\ No newline at end of file
diff --git a/Animation_Homepage.js b/Animation_Homepage.js
index c05a115..cfe39cf 100644
--- a/Animation_Homepage.js
+++ b/Animation_Homepage.js
@@ -1,20 +1,6 @@
-/* ==========================================
- ANIMATION HOMEPAGE - Homepage Controller
- ==========================================
- fitur utama:
- 1. updateAuthButton() - Toggle LOGIN/LOGOUT button
- 2. handleLogout() - Logout logic dengan PHP
- 3. Event listeners untuk semua button & keyboard
- 4. Smooth scroll & responsive handling
- ========================================== */
-
-// Animation Homepage.js
(function() {
'use strict';
- /* ==========================================
- DOM ELEMENTS - Cache semua element penting
- ========================================== */
const elements = {
logo: null,
authBtn: null,
@@ -26,37 +12,29 @@
logoutFailedOverlay: null
};
- /* ==========================================
- INITIALIZE - Setup saat page load
- ========================================== */
function init() {
if (document.readyState === 'loading') {
- // Kalau DOM belum ready, tunggu event
document.addEventListener('DOMContentLoaded', initAll);
} else {
- // Kalau sudah ready, langsung init
initAll();
}
}
function initAll() {
try {
- // Step 1: Cache semua DOM elements
+
cacheElements();
- // Step 2: Validasi element wajib ada
+
if (!validateElements()) {
console.error('Some required elements are missing');
return;
}
- // Step 3: Setup event listeners
setupEventListeners();
- // Step 4: Initialize smooth scroll
initSmoothScroll();
- // Step 5: Update button LOGIN/LOGOUT sesuai status
updateAuthButton();
console.log('✅ Homepage initialized successfully');
@@ -65,11 +43,6 @@
}
}
- /* ==========================================
- CACHE ELEMENTS - Simpan reference ke variable
- ==========================================
- Kenapa? Biar nggak query DOM berkali-kali (performa)
- ========================================== */
function cacheElements() {
elements.logo = document.querySelector('.logo');
elements.authBtn = document.getElementById('auth-button');
@@ -81,129 +54,89 @@
elements.logoutFailedOverlay = document.getElementById('logout-failed-overlay');
}
- /* ==========================================
- VALIDATE ELEMENTS - Cek element wajib ada
- ========================================== */
function validateElements() {
- // Element yang HARUS ada: logo, authBtn, playBtn
const requiredElements = ['logo', 'authBtn', 'playBtn'];
const missingElements = requiredElements.filter(key => !elements[key]);
if (missingElements.length > 0) {
console.warn('Missing elements:', missingElements);
- return false; // Gagal validasi
+ return false;
}
- return true; // Semua element ada
+ return true;
}
- /* ==========================================
- UPDATE AUTH BUTTON - Toggle LOGIN/LOGOUT
- ==========================================
- Cek dari localStorage & sessionStorage
- ========================================== */
function updateAuthButton() {
- // Cek apakah user sudah login
- // 3 cara cek: authToken, username, atau loggedInUser
const authToken = localStorage.getItem('authToken');
const username = localStorage.getItem('username');
const loggedInUser = sessionStorage.getItem('loggedInUser');
if (authToken || username || loggedInUser) {
- // User SUDAH LOGIN → show LOGOUT button
elements.authBtn.textContent = 'LOGOUT';
elements.authBtn.classList.add('logout-mode');
} else {
- // User BELUM LOGIN → show LOGIN button
elements.authBtn.textContent = 'LOGIN';
elements.authBtn.classList.remove('logout-mode');
}
}
-
- /* ==========================================
- SETUP EVENT LISTENERS - Bind semua event
- ========================================== */
+
function setupEventListeners() {
- // Logo click → reload homepage
if (elements.logo) {
elements.logo.addEventListener('click', handleLogoClick);
}
-
- // Auth button → login/logout
+ t
if (elements.authBtn) {
elements.authBtn.addEventListener('click', handleAuthClick);
}
- // Play button → go to game
if (elements.playBtn) {
elements.playBtn.addEventListener('click', handlePlayClick);
}
- // Leaderboard button
if (elements.leaderboardBtn) {
elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick);
}
- // Keyboard shortcuts (P, L, B)
document.addEventListener('keydown', handleKeyPress);
- // Window events
window.addEventListener('resize', handleResize);
window.addEventListener('beforeunload', handleBeforeUnload);
}
- /* ==========================================
- EVENT HANDLERS
- ========================================== */
-
function handleLogoClick(e) {
e.preventDefault();
window.location.href = 'Homepage.html';
}
- /* ==========================================
- HANDLE AUTH CLICK - Login atau Logout
- ========================================== */
function handleAuthClick(e) {
- // Cek status login dari localStorage & sessionStorage
const authToken = localStorage.getItem('authToken');
const username = localStorage.getItem('username');
const loggedInUser = sessionStorage.getItem('loggedInUser');
if (authToken || username || loggedInUser) {
- // User sudah login → LOGOUT
handleLogout();
} else {
- // User belum login → ke halaman LOGIN
window.location.href = 'Login.html';
}
}
- /* ==========================================
- HANDLE LOGOUT - Logout Logic dengan PHP
- ========================================== */
async function handleLogout() {
try {
- // Step 1: Panggil PHP untuk hapus session di server
const response = await fetch("http://localhost/Kelompok06_2048/Logout.php", {
method: "POST"
});
const data = await response.json();
- // Step 2: Hapus token & username dari localStorage
localStorage.removeItem("authToken");
localStorage.removeItem("username");
- // Step 3: Hapus juga dari sessionStorage
sessionStorage.removeItem('loggedInUser');
sessionStorage.removeItem('showTutorial');
- // Step 4: Show success modal
if (elements.logoutOverlay) {
elements.logoutOverlay.style.display = 'flex';
- // Auto close setelah 2 detik & redirect
setTimeout(() => {
elements.logoutOverlay.style.display = 'none';
window.location.href = "Homepage.html";
@@ -213,17 +146,14 @@
} catch (error) {
console.error('Logout failed:', error);
- // Tetap hapus data lokal meski server error
localStorage.removeItem("authToken");
localStorage.removeItem("username");
sessionStorage.removeItem('loggedInUser');
sessionStorage.removeItem('showTutorial');
- // Show error modal
if (elements.logoutFailedOverlay) {
elements.logoutFailedOverlay.style.display = 'flex';
- // Auto close setelah 2.5 detik
setTimeout(() => {
elements.logoutFailedOverlay.style.display = 'none';
window.location.href = "Homepage.html";
@@ -233,34 +163,26 @@
}
function handlePlayClick(e) {
- // Allow default behavior (navigate ke 2048.html)
console.log('Starting game...');
}
function handleLeaderboardClick(e) {
- // Allow default behavior (navigate ke leaderboard.html)
console.log('Opening leaderboard...');
}
- /* ==========================================
- KEYBOARD SHORTCUTS
- ========================================== */
function handleKeyPress(e) {
- // Press 'P' → Play game
if (e.key === 'p' || e.key === 'P') {
if (elements.playBtn) {
elements.playBtn.click();
}
}
- // Press 'L' → Login/Logout
if (e.key === 'l' || e.key === 'L') {
if (elements.authBtn) {
elements.authBtn.click();
}
}
- // Press 'B' → Leaderboard (Board)
if (e.key === 'b' || e.key === 'B') {
if (elements.leaderboardBtn) {
elements.leaderboardBtn.click();
@@ -269,7 +191,6 @@
}
function handleResize() {
- // Handle responsive behavior (mobile, tablet, desktop)
const width = window.innerWidth;
if (width < 768) {
@@ -282,13 +203,9 @@
}
function handleBeforeUnload(e) {
- // Cleanup sebelum page unload
console.log('Page unloading...');
}
- /* ==========================================
- SMOOTH SCROLL - Untuk anchor links (#)
- ========================================== */
function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
@@ -308,9 +225,6 @@
});
}
- /* ==========================================
- UTILITY FUNCTIONS
- ========================================== */
function checkBrowserSupport() {
const features = {
localStorage: typeof(Storage) !== 'undefined',
@@ -326,9 +240,6 @@
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
- /* ==========================================
- PUBLIC API - Fungsi yang bisa dipanggil dari luar
- ========================================== */
window.Homepage = {
init: initAll,
isMobile: isMobile,
@@ -336,11 +247,7 @@
updateAuthButton: updateAuthButton
};
- /* ==========================================
- CLEANUP - Remove semua event listener
- ========================================== */
window.cleanupHomepage = function() {
- // Remove event listeners (untuk prevent memory leak)
if (elements.logo) {
elements.logo.removeEventListener('click', handleLogoClick);
}
@@ -364,7 +271,6 @@
console.log('Homepage cleaned up');
};
- // Start initialization saat script load
init();
-})(); // IIFE - langsung execute
\ No newline at end of file
+})();
\ No newline at end of file
diff --git a/Animation_Leaderboard.js b/Animation_Leaderboard.js
index 437af15..b6b55f1 100644
--- a/Animation_Leaderboard.js
+++ b/Animation_Leaderboard.js
@@ -1,4 +1,3 @@
-// Particles Animation
const particlesContainer = document.getElementById('particles');
function createParticle() {
diff --git a/Animation_Login.js b/Animation_Login.js
index b5f4fd2..4eb3cd1 100644
--- a/Animation_Login.js
+++ b/Animation_Login.js
@@ -1,150 +1,92 @@
-/* ==========================================
- ANIMATION LOGIN - PARTICLE BACKGROUND SYSTEM
- ==========================================
- fungsi:
- - Class Particle: objek partikel dengan posisi & kecepatan
- - Even Distribution: distribusi partikel merata (15x10 grid)
- - Animate Loop: pergerakan smooth dengan requestAnimationFrame
- ========================================== */
-
-
-/* ==========================================
- SETUP CONTAINER & CONFIG
- ========================================== */
const particlesContainer = document.getElementById('particles');
-const particleCount = 150; // Total partikel yang dibuat
-const particles = []; // Array untuk simpan semua partikel
+const particleCount = 150;
+const particles = [];
-/* ==========================================
- CLASS PARTICLE - Blueprint untuk setiap partikel
- ==========================================
- Properties:
- - x, y: Posisi partikel
- - vx, vy: Kecepatan horizontal & vertikal (velocity)
- - size: Ukuran partikel (2-5px)
- - color: Warna neon random
- ========================================== */
class Particle {
constructor() {
- // Buat element DOM untuk partikel
this.element = document.createElement('div');
this.element.className = 'particle';
this.reset(); // Initialize posisi & properti
particlesContainer.appendChild(this.element);
}
- /* ==========================================
- RESET - Set/reset properti partikel
- ========================================== */
reset() {
- // Posisi random di seluruh layar
+
this.x = Math.random() * window.innerWidth;
- this.y = Math.random() * window.innerHeight;
-
- // Kecepatan random (-0.6 sampai +0.6 pixel/frame)
+ this.y = Math.random() * window.innerHeight;
+
this.vx = (Math.random() - 0.5) * 1.2;
this.vy = (Math.random() - 0.5) * 1.2;
- // Ukuran random (2-5px)
this.size = Math.random() * 3 + 2;
- // Array warna neon untuk partikel
const colors = [
- '#00d9ff', // Cyan
- '#ff00ff', // Magenta
- '#00ffff', // Cyan bright
- '#ff0080', // Pink
- '#9d00ff', // Purple
- '#00ff88' // Green
+ '#00d9ff',
+ '#ff00ff',
+ '#00ffff',
+ '#ff0080',
+ '#9d00ff',
+ '#00ff88'
];
- // Pilih warna random dari array
const color = colors[Math.floor(Math.random() * colors.length)];
this.element.style.background = color;
- this.element.style.boxShadow = `0 0 15px ${color}`; // Glow effect
+ this.element.style.boxShadow = `0 0 15px ${color}`;
this.element.style.width = `${this.size}px`;
this.element.style.height = `${this.size}px`;
- this.updatePosition(); // Update posisi di DOM
+ this.updatePosition();
}
- /* ==========================================
- UPDATE POSITION - Sync posisi ke DOM
- ========================================== */
updatePosition() {
this.element.style.left = `${this.x}px`;
this.element.style.top = `${this.y}px`;
}
- /* ==========================================
- MOVE - Update posisi berdasarkan velocity
- ==========================================
- Fitur: Wrap-around screen (partikel keluar = muncul sisi lain)
- ========================================== */
move() {
- // Update posisi berdasarkan kecepatan
+
this.x += this.vx;
this.y += this.vy;
- // WRAP-AROUND LOGIC
- // Kalau keluar dari kiri → muncul dari kanan
if (this.x < -10) this.x = window.innerWidth + 10;
- // Kalau keluar dari kanan → muncul dari kiri
+
if (this.x > window.innerWidth + 10) this.x = -10;
- // Kalau keluar dari atas → muncul dari bawah
+
if (this.y < -10) this.y = window.innerHeight + 10;
- // Kalau keluar dari bawah → muncul dari atas
+
if (this.y > window.innerHeight + 10) this.y = -10;
- this.updatePosition(); // Sync ke DOM
+ this.updatePosition();
}
}
-/* ==========================================
- EVEN DISTRIBUTION - Distribusi Partikel Merata
- ==========================================
- Konsep: Grid 15 kolom x 10 baris (total 150 partikel)
- Biar nggak menumpuk di satu area
- ========================================== */
-const cols = 15; // Jumlah kolom
-const rows = 10; // Jumlah baris
-let particleIndex = 0; // Counter partikel yang sudah dibuat
+const cols = 15;
+const rows = 10;
+let particleIndex = 0;
-// Loop baris
for (let i = 0; i < rows; i++) {
- // Loop kolom
+
for (let j = 0; j < cols; j++) {
- // Guard: stop kalau sudah 150 partikel
+
if (particleIndex >= particleCount) break;
const particle = new Particle();
- // DISTRIBUSI MERATA + RANDOM OFFSET
- // Formula: (index / total) × lebar/tinggi layar
particle.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100;
particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100;
particle.updatePosition();
- particles.push(particle); // Simpan ke array
+ particles.push(particle);
particleIndex++;
}
if (particleIndex >= particleCount) break;
}
-/* ==========================================
- ANIMATE - Main Animation Loop
- ==========================================
- Menggunakan requestAnimationFrame untuk performa optimal
- (60 FPS, smooth, GPU-accelerated)
- ========================================== */
function animate() {
- // Loop semua partikel dan gerakkan
+
particles.forEach(p => p.move());
- // Request next frame (recursive call)
- // Browser otomatis sync dengan refresh rate monitor
requestAnimationFrame(animate);
}
-// Start animation loop
animate();
\ No newline at end of file
diff --git a/Animation_Register.js b/Animation_Register.js
index 9a24f9f..9f179e6 100644
--- a/Animation_Register.js
+++ b/Animation_Register.js
@@ -1,4 +1,3 @@
-// Particle System
const particlesContainer = document.getElementById('particles');
const particleCount = 150;
const particles = [];
@@ -50,7 +49,6 @@ class Particle {
}
}
-// Create particles with even distribution
const cols = 15;
const rows = 10;
let particleIndex = 0;
@@ -61,7 +59,6 @@ for (let i = 0; i < rows; i++) {
const particle = new Particle();
- // Even distribution + slight random offset
particle.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100;
particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100;
particle.updatePosition();
@@ -72,7 +69,6 @@ for (let i = 0; i < rows; i++) {
if (particleIndex >= particleCount) break;
}
-// Animate
function animate() {
particles.forEach(p => p.move());
requestAnimationFrame(animate);
diff --git a/Connection.php b/Connection.php
index 9e10452..b8b01b2 100644
--- a/Connection.php
+++ b/Connection.php
@@ -1,11 +1,7 @@
connect_error) {
http_response_code(500);
diff --git a/Homepage.css b/Homepage.css
index 7f57888..54cbab6 100644
--- a/Homepage.css
+++ b/Homepage.css
@@ -20,7 +20,6 @@ body {
max-width: 100vw;
}
-/* Animated Background Grid */
body::before {
content: '';
position: fixed;
@@ -42,7 +41,6 @@ body::before {
100% { transform: translate(50px, 50px); }
}
-/* Neon Particles */
#particles {
position: fixed;
top: 0;
@@ -75,32 +73,25 @@ body::before {
}
}
-/* Atur lebar scrollbar */
::-webkit-scrollbar {
width: 12px;
}
-/* Background jalanan scrollbar (Track) - Gelap agar neon menyala */
::-webkit-scrollbar-track {
- background: #0b0b15; /* Hitam keunguan sangat gelap */
+ background: #0b0b15;
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/Homepage.html b/Homepage.html
index c8357b7..bd58c2d 100644
--- a/Homepage.html
+++ b/Homepage.html
@@ -13,20 +13,14 @@
-
-
-
-
-
-
2048
Join the tiles, reach 2048
@@ -36,8 +30,6 @@
@@ -79,9 +66,7 @@
You have been logged out successfully
-
diff --git a/Homepage.js b/Homepage.js
index db7c356..b590cfa 100644
--- a/Homepage.js
+++ b/Homepage.js
@@ -1,17 +1,15 @@
(function() {
'use strict';
- // ==================== DOM ELEMENTS ====================
const elements = {
logo: null,
- loginBtn: null, // Ini akan di-update oleh logout.js
+ loginBtn: null,
playBtn: null,
leaderboardBtn: null,
heroTitle: null,
heroSubtitle: null
};
- // ==================== INITIALIZE ====================
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
@@ -22,19 +20,15 @@
function initAll() {
try {
- // Cache DOM elements
cacheElements();
- // Validate elements
if (!validateElements()) {
console.error('Some required elements are missing');
return;
}
- // Setup event listeners
setupEventListeners();
- // Initialize smooth scroll
initSmoothScroll();
console.log('✅ Homepage initialized successfully');
@@ -43,11 +37,8 @@
}
}
- // ==================== CACHE ELEMENTS ====================
function cacheElements() {
elements.logo = document.querySelector('.logo');
- // ✅ PERBAIKAN: Jangan cache loginBtn, biar logout.js yang handle
- // Cek apakah button Login atau Logout
elements.loginBtn = document.querySelector('.btn-login') || document.querySelector('.btn-logout');
elements.playBtn = document.querySelector('a[href="2048.html"]');
elements.leaderboardBtn = document.querySelector('a[href="leaderboard.html"]');
@@ -55,7 +46,6 @@
elements.heroSubtitle = document.querySelector('.hero-subtitle');
}
- // ==================== VALIDATE ELEMENTS ====================
function validateElements() {
const requiredElements = ['logo', 'playBtn'];
const missingElements = requiredElements.filter(key => !elements[key]);
@@ -68,59 +58,46 @@
return true;
}
- // ==================== EVENT LISTENERS ====================
function setupEventListeners() {
- // Logo click - reload page
+
if (elements.logo) {
elements.logo.addEventListener('click', handleLogoClick);
}
- // ✅ PERBAIKAN: JANGAN tambah event listener ke loginBtn
- // Biar logout.js yang handle login/logout button
-
- // Play button
if (elements.playBtn) {
elements.playBtn.addEventListener('click', handlePlayClick);
}
- // Leaderboard button
if (elements.leaderboardBtn) {
elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick);
}
- // Keyboard shortcuts
document.addEventListener('keydown', handleKeyPress);
-
- // Window events
+
window.addEventListener('resize', handleResize);
window.addEventListener('beforeunload', handleBeforeUnload);
}
- // ==================== EVENT HANDLERS ====================
function handleLogoClick(e) {
e.preventDefault();
window.location.href = 'Homepage.html';
}
function handlePlayClick(e) {
- // Allow default behavior (navigate to 2048.html)
console.log('Starting game...');
}
function handleLeaderboardClick(e) {
- // Allow default behavior (navigate to leaderboard.html)
console.log('Opening leaderboard...');
}
function handleKeyPress(e) {
- // Press 'P' to play
if (e.key === 'p' || e.key === 'P') {
if (elements.playBtn) {
elements.playBtn.click();
}
}
- // Press 'L' to login (hanya jika belum login)
if (e.key === 'l' || e.key === 'L') {
const loginBtn = document.querySelector('.btn-login');
if (loginBtn) {
@@ -128,7 +105,6 @@
}
}
- // Press 'B' for leaderboard
if (e.key === 'b' || e.key === 'B') {
if (elements.leaderboardBtn) {
elements.leaderboardBtn.click();
@@ -137,7 +113,6 @@
}
function handleResize() {
- // Handle responsive behavior if needed
const width = window.innerWidth;
if (width < 768) {
@@ -150,11 +125,9 @@
}
function handleBeforeUnload(e) {
- // Cleanup before page unload
console.log('Page unloading...');
}
- // ==================== SMOOTH SCROLL ====================
function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
@@ -174,7 +147,6 @@
});
}
- // ==================== UTILITY FUNCTIONS ====================
function checkBrowserSupport() {
const features = {
localStorage: typeof(Storage) !== 'undefined',
@@ -190,16 +162,14 @@
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
- // ==================== PUBLIC API ====================
window.Homepage = {
init: initAll,
isMobile: isMobile,
checkSupport: checkBrowserSupport
};
- // ==================== CLEANUP ====================
window.cleanupHomepage = function() {
- // Remove event listeners
+
if (elements.logo) {
elements.logo.removeEventListener('click', handleLogoClick);
}
@@ -219,7 +189,6 @@
console.log('Homepage cleaned up');
};
- // Start initialization
init();
})();
\ No newline at end of file
diff --git a/Homepage_Button.css b/Homepage_Button.css
index b7c9c78..74f0b13 100644
--- a/Homepage_Button.css
+++ b/Homepage_Button.css
@@ -1,4 +1,3 @@
-/* General Button Styles */
.btn {
padding: 14px 35px;
border: none;
@@ -35,7 +34,6 @@
height: 300px;
}
-/* Primary & Login Buttons */
.btn-primary {
background: linear-gradient(135deg, #00eaff, #ff00ff);
color: #fff;
@@ -73,7 +71,6 @@
border-color: rgba(0, 255, 255, 0.8);
}
-/* CTA Buttons (Play & Leaderboard) */
.btn-cta {
padding: 18px 45px;
font-size: 19px;
@@ -127,7 +124,6 @@
box-shadow: 0 12px 50px rgba(255, 215, 0, 0.9), 0 0 40px rgba(255, 170, 0, 0.7);
}
-/* Logout Specific Buttons */
.btn-logout {
background: linear-gradient(135deg, #ff0066, #cc0033) !important;
color: #fff !important;
diff --git a/Homepage_Credit.css b/Homepage_Credit.css
index c2008ef..f29ecea 100644
--- a/Homepage_Credit.css
+++ b/Homepage_Credit.css
@@ -1,4 +1,3 @@
-/* Main Content Wrapper - Takes all space except footer */
.main-content {
flex: 1;
display: flex;
@@ -7,7 +6,6 @@
z-index: 10;
}
-/* Footer Section - Always at bottom */
footer {
position: relative;
z-index: 10;
@@ -17,7 +15,6 @@ footer {
margin-top: 0;
}
-/* HR Line Above Credits */
.footer-hr {
width: 50%;
max-width: 500px;
@@ -27,7 +24,6 @@ footer {
border: none;
}
-/* Credits Container */
.credits {
max-width: 100%;
margin: 0 auto;
@@ -36,7 +32,6 @@ footer {
border: none;
}
-/* Credit Text */
.credits-text {
color: rgba(255, 255, 255, 0.5);
font-size: 15px;
@@ -51,7 +46,6 @@ footer {
font-weight: 500;
}
-/* Responsive - HP Only */
@media (max-width: 480px) {
.main-content {
min-height: calc(100vh - 80px);
diff --git a/Homepage_Header.css b/Homepage_Header.css
index 1394ab0..4192b12 100644
--- a/Homepage_Header.css
+++ b/Homepage_Header.css
@@ -1,4 +1,3 @@
-/* Header Container */
header {
position: relative;
z-index: 10;
@@ -12,7 +11,6 @@ header {
box-shadow: 0 4px 30px rgba(0, 234, 255, 0.2);
}
-/* Logo Styles */
.logo {
font-size: 36px;
font-weight: 900;
diff --git a/Homepage_Hero.css b/Homepage_Hero.css
index 8bc5b12..f68a187 100644
--- a/Homepage_Hero.css
+++ b/Homepage_Hero.css
@@ -91,7 +91,6 @@
to { opacity: 1; transform: translateY(0); }
}
-/* Floating Decoration Elements */
.hero::before {
content: '';
position: absolute;
diff --git a/Homepage_Modal.css b/Homepage_Modal.css
index 7050e93..adce8b4 100644
--- a/Homepage_Modal.css
+++ b/Homepage_Modal.css
@@ -1,4 +1,3 @@
-/* Logout Confirmation Modal */
.logout-overlay {
position: fixed;
inset: 0;
@@ -12,7 +11,9 @@
transition: opacity 0.3s ease;
}
-.logout-overlay.active { display: flex; opacity: 1; }
+.logout-overlay.active {
+ display: flex; opacity: 1;
+}
.logout-modal {
background: linear-gradient(145deg, rgba(30, 0, 50, 0.98), rgba(45, 0, 70, 0.98));
@@ -90,7 +91,6 @@
justify-content: center;
}
-/* Logout Success Modal */
.logout-success-overlay {
position: fixed;
inset: 0;
diff --git a/Homepage_Responsive.css b/Homepage_Responsive.css
index c736c4c..1e2ef8d 100644
--- a/Homepage_Responsive.css
+++ b/Homepage_Responsive.css
@@ -4,31 +4,26 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
- gap: 0; /* Gap tidak wajib jika sudah space-between */
+ gap: 0;
}
.logo {
- /* Sesuaikan ukuran font agar muat dalam satu baris */
font-size: 24px;
letter-spacing: 2px;
margin: 0;
}
- /* Masukkan ini di dalam @media (max-width: 480px) */
-
.btn-login,
.btn-logout {
- padding: 6px 14px !important; /* Ukuran tombol jauh lebih kecil */
- font-size: 11px !important; /* Ukuran teks lebih kecil */
- letter-spacing: 1px; /* Jarak antar huruf dirapatkan */
- border-radius: 8px; /* Lengkungan sudut disesuaikan */
- min-width: auto; /* Mencegah tombol jadi terlalu lebar */
- height: auto; /* Tinggi mengikuti isi teks */
+ padding: 6px 14px !important;
+ font-size: 11px !important;
+ letter-spacing: 1px;
+ border-radius: 8px;
+ min-width: auto;
+ height: auto;
line-height: normal;
}
-
- /* Hero Section - CENTERED & BALANCED */
.hero {
padding: 50px 15px;
display: flex;
@@ -59,7 +54,6 @@
gap: 15px;
}
- /* Logout Modal Responsive */
.logout-modal,
.logout-success-modal {
padding: 40px 30px;
@@ -85,12 +79,9 @@
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 */
+ width: 6px !important;
background: #0b0b15;
}
@@ -101,9 +92,9 @@
html::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb {
- background-color: #bc13fe !important; /* Warna Magenta */
+ background-color: #bc13fe !important;
border-radius: 10px;
border: none;
- box-shadow: 0 0 8px #bc13fe !important; /* Glow Neon */
+ box-shadow: 0 0 8px #bc13fe !important;
}
}
\ No newline at end of file
diff --git a/Homepage_Scrollbar.css b/Homepage_Scrollbar.css
deleted file mode 100644
index 3d767d9..0000000
--- a/Homepage_Scrollbar.css
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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.css b/Leaderboard.css
index e67eb43..3a756f4 100644
--- a/Leaderboard.css
+++ b/Leaderboard.css
@@ -1,4 +1,3 @@
-/* Main Container */
.container {
height: auto !important;
max-height: none !important;
@@ -38,7 +37,6 @@
}
}
-/* Title */
h1 {
text-align: center;
font-size: 2.2rem;
@@ -63,7 +61,6 @@ h1::before {
filter: drop-shadow(0 0 10px #ffd700);
}
-/* Stats Layout */
.stats-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -71,7 +68,6 @@ h1::before {
margin-bottom: 30px;
}
-/* Leaderboard List Structure */
.leaderboard-list {
list-style: none;
display: flex;
@@ -106,7 +102,6 @@ h1::before {
box-shadow: 0 0 15px rgba(0, 234, 255, 0.8);
}
-/* Leaderboard Item Wrapper */
.leaderboard-item {
display: flex;
align-items: center;
@@ -125,9 +120,6 @@ h1::before {
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
}
-/* === RANK LOGIC & THEMES === */
-
-/* RANK 1: CYAN NEON */
.leaderboard-item.rank-1,
.your-rank-container.rank-1 .your-rank-item {
background: linear-gradient(90deg, rgba(0, 234, 255, 0.25) 0%, rgba(0, 234, 255, 0.05) 100%);
@@ -161,7 +153,6 @@ h1::before {
filter: none !important;
}
-/* RANK 2: MAGENTA NEON */
.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%);
@@ -184,7 +175,6 @@ h1::before {
color: #ff00ff;
}
-/* RANK 3: VIOLET NEON */
.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%);
@@ -207,7 +197,6 @@ h1::before {
color: #a855f7;
}
-/* RANK OTHER */
.leaderboard-item.rank-other {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
@@ -219,7 +208,6 @@ h1::before {
border: 1px solid rgba(0, 255, 255, 0.3);
}
-/* Your Rank Section Layout */
.your-rank-container {
background: transparent;
border: none;
@@ -234,7 +222,7 @@ h1::before {
.your-rank-container::before {
content: '';
position: absolute;
- top: -1px; /* Pas di garis border */
+ top: -1px;
left: 50%;
transform: translateX(-50%);
width: 50px;
@@ -253,4 +241,4 @@ h1::before {
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);
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
-}
+}
\ No newline at end of file
diff --git a/Leaderboard.html b/Leaderboard.html
index c7690d2..81fb4ac 100644
--- a/Leaderboard.html
+++ b/Leaderboard.html
@@ -9,25 +9,18 @@
-
-
-
-
diff --git a/Leaderboard.js b/Leaderboard.js
index e8eee5c..4a3ee69 100644
--- a/Leaderboard.js
+++ b/Leaderboard.js
@@ -61,8 +61,6 @@ function renderUserRank(user) {
container.innerHTML = html;
}
-// ... fungsi escapeHtml tetap sama ...
-
function renderLeaderboard(players) {
const listContainer = document.getElementById('leaderboardList');
if (!listContainer) return; // Safety check
diff --git a/Leaderboard.php b/Leaderboard.php
index dc9463d..a56a4ab 100644
--- a/Leaderboard.php
+++ b/Leaderboard.php
@@ -1,17 +1,14 @@
"error",
"leaderboard" => [],
"user_rank" => null
];
-// Ambil Top 10 Leaderboard Global
-// Urut score terbesar
$query = "
SELECT username, score
FROM leaderboard
@@ -26,11 +23,9 @@ if ($result && $result->num_rows > 0) {
}
}
-// Ambil Ranking User yang Sedang Login (your rank)
if (isset($_SESSION['user_id'])) {
$my_id = $_SESSION['user_id'];
- // Ambil username & score user
$scoreQuery = $conn->prepare("
SELECT username, score
FROM leaderboard
@@ -44,7 +39,6 @@ if (isset($_SESSION['user_id'])) {
$myScore = $scoreRow['score'];
$myUsername = $scoreRow['username'];
- // Hitung jumlah user yang berada di atas
$rankQuery = $conn->prepare("
SELECT COUNT(*) AS rank_above
FROM leaderboard
@@ -56,7 +50,6 @@ if (isset($_SESSION['user_id'])) {
$rankResult = $rankQuery->get_result();
$rankRow = $rankResult->fetch_assoc();
- // Rank = jumlah di atas + 1
$response['user_rank'] = [
"username" => $myUsername,
"score" => $myScore,
@@ -65,12 +58,9 @@ if (isset($_SESSION['user_id'])) {
}
}
-// 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/Leaderboard_Base.css b/Leaderboard_Base.css
index 0906738..a4f19be 100644
--- a/Leaderboard_Base.css
+++ b/Leaderboard_Base.css
@@ -1,5 +1,3 @@
-/* Leaderboard_Base.css */
-
* {
margin: 0;
padding: 0;
@@ -20,7 +18,6 @@ body {
position: relative;
}
-/* Neon Particles */
#particles {
position: fixed;
top: 0;
@@ -40,30 +37,23 @@ body {
pointer-events: none;
}
-/* Leaderboard_Base.css - Bagian Paling Bawah */
-
-/* === KUSTOMISASI SCROLLBAR BROWSER UTAMA === */
-/* Lebar Scrollbar */
::-webkit-scrollbar {
width: 12px;
- background: #0b0015; /* Latar belakang track gelap */
+ background: #0b0015;
}
-/* Track (Jalur) */
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-left: 1px solid rgba(255, 255, 255, 0.05);
}
-/* Thumb (Batang Scroll) */
::-webkit-scrollbar-thumb {
- background: linear-gradient(180deg, #3b0066, #ff00ff); /* Gradasi Ungu ke Pink */
+ background: linear-gradient(180deg, #3b0066, #ff00ff);
border-radius: 6px;
- border: 3px solid #0b0015; /* Memberi jarak agar terlihat melayang */
+ border: 3px solid #0b0015;
}
-/* Efek Hover di Scrollbar */
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #5e00a3, #ff33ff);
- box-shadow: 0 0 15px rgba(255, 0, 255, 0.5); /* Efek glow saat disentuh */
+ box-shadow: 0 0 15px rgba(255, 0, 255, 0.5);
}
\ No newline at end of file
diff --git a/Login.css b/Login.css
index 06e4241..a14a0e1 100644
--- a/Login.css
+++ b/Login.css
@@ -146,7 +146,6 @@ input::placeholder {
text-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
}
-/* Custom Modal Styles */
.modal {
display: none;
position: fixed;
@@ -168,18 +167,15 @@ input::placeholder {
.modal-content {
background: linear-gradient(135deg, rgba(30, 0, 50, 0.95) 0%, rgba(50, 0, 80, 0.95) 100%);
- /* Border diubah menjadi lebih terang */
border: 2px solid rgba(0, 217, 255, 0.3);
border-radius: 20px;
padding: 40px 30px;
text-align: center;
max-width: 400px;
width: 90%;
- /* Box shadow ditambahkan efek neon (luar dan dalam) */
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5),
0 0 40px rgba(0, 217, 255, 0.3),
inset 0 0 30px rgba(0, 217, 255, 0.1);
- /* Animasi digabungkan: slideIn untuk muncul, glowBorder untuk efek neon berkedip */
animation: slideIn 0.3s ease, glowBorder 3s ease-in-out infinite;
}
@@ -275,12 +271,10 @@ input::placeholder {
}
}
-/* Autocomplete/Datalist Styling */
input::-webkit-calendar-picker-indicator {
filter: invert(1);
}
-/* Styling untuk browser autocomplete */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
@@ -294,7 +288,6 @@ input:-webkit-autofill:focus {
border-color: #00d9ff;
}
-/* Styling untuk datalist dropdown - terbatas karena browser restrictions */
datalist {
background-color: rgba(30, 0, 50, 0.95);
}
@@ -305,12 +298,10 @@ option {
padding: 10px;
}
-/* Untuk Firefox */
input::-moz-list-thumbnail {
background-color: rgba(30, 0, 50, 0.95);
}
-/* Custom styling untuk dropdown suggestion */
input[list]::-webkit-list-button {
color: #00d9ff;
}
diff --git a/Login.html b/Login.html
index 35b892b..9c05367 100644
--- a/Login.html
+++ b/Login.html
@@ -8,15 +8,12 @@
-
-
-
diff --git a/Login.js b/Login.js
index 4e1e3ea..c4f67cf 100644
--- a/Login.js
+++ b/Login.js
@@ -1,7 +1,6 @@
import { showModal, setupModalOk, setupOutsideClose } from "./Modal_Login.js";
import { loginRequest } from "./Login_Request.js";
-// ✅ Setup modal saat halaman load
document.addEventListener('DOMContentLoaded', function() {
setupModalOk();
setupOutsideClose();
@@ -27,7 +26,6 @@ document.getElementById('loginForm').addEventListener('submit', async function(e
console.log('Response dari server:', data);
if (data.success) {
- // ✅ PERBAIKAN: Gunakan sessionStorage (sama dengan logout.js)
sessionStorage.setItem('authToken', data.token);
sessionStorage.setItem('loggedInUser', data.username);
diff --git a/Logout.php b/Logout.php
index b0000eb..1ee330f 100644
--- a/Logout.php
+++ b/Logout.php
@@ -7,6 +7,6 @@ session_destroy();
echo json_encode([
"status" => "success",
- "message" => "Logout berhasil"
+ "message" => "Logout successful."
]);
?>
\ No newline at end of file
diff --git a/Register.html b/Register.html
index 533a929..3edfbc6 100644
--- a/Register.html
+++ b/Register.html
@@ -13,10 +13,8 @@
-
REGISTER
-
-
Already Have An Account?
Login
-
-
-
diff --git a/Register_Autofill.css b/Register_Autofill.css
index f90387c..d1235e7 100644
--- a/Register_Autofill.css
+++ b/Register_Autofill.css
@@ -1,10 +1,7 @@
-/* register-autofill.css */
-/* Autocomplete/Datalist Styling */
input::-webkit-calendar-picker-indicator {
filter: invert(1);
}
-/* Browser autocomplete styling */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
@@ -18,7 +15,6 @@ input:-webkit-autofill:focus {
border-color: #00d9ff;
}
-/* Datalist dropdown styling - limited due to browser restrictions */
datalist {
background-color: rgba(30, 0, 50, 0.95);
}
@@ -29,12 +25,10 @@ option {
padding: 10px;
}
-/* For Firefox */
input::-moz-list-thumbnail {
background-color: rgba(30, 0, 50, 0.95);
}
-/* Custom styling for dropdown suggestion */
input[list]::-webkit-list-button {
color: #00d9ff;
}
\ No newline at end of file
diff --git a/Register_Base.css b/Register_Base.css
index 5b8e2e2..21fed72 100644
--- a/Register_Base.css
+++ b/Register_Base.css
@@ -1,4 +1,3 @@
-/* register-base.css */
* {
margin: 0;
padding: 0;
@@ -16,7 +15,6 @@ body {
position: relative;
}
-/* Neon Particles */
#particles {
position: fixed;
top: 0;
diff --git a/Register_Container.css b/Register_Container.css
index 12fd3af..475f2c4 100644
--- a/Register_Container.css
+++ b/Register_Container.css
@@ -1,4 +1,3 @@
-/* register-container.css */
.container {
position: relative;
z-index: 2;