Update
This commit is contained in:
parent
f87ca734de
commit
f208ac7782
23
2048.css
23
2048.css
@ -8,7 +8,6 @@ body {
|
|||||||
background: radial-gradient(circle at 20% 20%, #3b0066, #0c001a 70%);
|
background: radial-gradient(circle at 20% 20%, #3b0066, #0c001a 70%);
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -75,9 +74,7 @@ body::after {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ======================
|
|
||||||
GAME CONTAINER
|
|
||||||
====================== */
|
|
||||||
.game-container {
|
.game-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
@ -88,7 +85,6 @@ body::after {
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tutorial Button Specific */
|
|
||||||
.btn-tutorial {
|
.btn-tutorial {
|
||||||
background: rgba(50, 0, 70, 0.85);
|
background: rgba(50, 0, 70, 0.85);
|
||||||
border-color: rgba(200, 100, 255, 0.45);
|
border-color: rgba(200, 100, 255, 0.45);
|
||||||
@ -111,9 +107,6 @@ body::after {
|
|||||||
color: rgba(200, 100, 255, 1);
|
color: rgba(200, 100, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================
|
|
||||||
BOARD
|
|
||||||
========================== */
|
|
||||||
#board {
|
#board {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: min(480px, 85vmin);
|
max-width: min(480px, 85vmin);
|
||||||
@ -167,9 +160,6 @@ body::after {
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ======================
|
|
||||||
TILE
|
|
||||||
====================== */
|
|
||||||
.tile {
|
.tile {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -218,7 +208,6 @@ body::after {
|
|||||||
75% { transform: translateX(10px); }
|
75% { transform: translateX(10px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced Merge Animation */
|
|
||||||
.tile.merge {
|
.tile.merge {
|
||||||
animation: mergeBounce 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
animation: mergeBounce 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@ -247,7 +236,6 @@ body::after {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Particle Effect */
|
|
||||||
.merge-particle {
|
.merge-particle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@ -262,7 +250,6 @@ body::after {
|
|||||||
filter: blur(0.5px);
|
filter: blur(0.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Score Popup Animation */
|
|
||||||
.score-popup {
|
.score-popup {
|
||||||
animation: scoreFloat 1s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
animation: scoreFloat 1s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||||
}
|
}
|
||||||
@ -282,7 +269,6 @@ body::after {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced Tile Glow on Merge */
|
|
||||||
.tile.merge {
|
.tile.merge {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 30px currentColor,
|
0 0 30px currentColor,
|
||||||
@ -290,7 +276,6 @@ body::after {
|
|||||||
inset 0 0 20px rgba(255, 255, 255, 0.3) !important;
|
inset 0 0 20px rgba(255, 255, 255, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smoother New Tile Animation */
|
|
||||||
.tile.new {
|
.tile.new {
|
||||||
animation: popIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
animation: popIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
}
|
}
|
||||||
@ -312,14 +297,12 @@ body::after {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tile Hover Effect - Make it more interactive */
|
|
||||||
.tile:not(:empty):hover {
|
.tile:not(:empty):hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
filter: brightness(1.2);
|
filter: brightness(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add ripple effect on board when moving */
|
|
||||||
#board.moving {
|
#board.moving {
|
||||||
animation: boardPulse 0.3s ease-out;
|
animation: boardPulse 0.3s ease-out;
|
||||||
}
|
}
|
||||||
@ -333,12 +316,10 @@ body::after {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show/Hide based on device */
|
|
||||||
.mobile-controls {
|
.mobile-controls {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keys Display */
|
|
||||||
.keys-container {
|
.keys-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -405,7 +386,6 @@ body::after {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Swipe Demo */
|
|
||||||
.swipe-demo {
|
.swipe-demo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -430,7 +410,6 @@ body::after {
|
|||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Objective Text */
|
|
||||||
.objective-text {
|
.objective-text {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
|||||||
42
2048.html
42
2048.html
@ -21,20 +21,13 @@
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Background Effects -->
|
|
||||||
<div class="particles" aria-hidden="true"></div>
|
<div class="particles" aria-hidden="true"></div>
|
||||||
<div class="floating-particles" id="floating-particles" aria-hidden="true"></div>
|
<div class="floating-particles" id="floating-particles" aria-hidden="true"></div>
|
||||||
<div class="starfield" aria-hidden="true"></div>
|
<div class="starfield" aria-hidden="true"></div>
|
||||||
<div class="cursor-light" aria-hidden="true"></div>
|
<div class="cursor-light" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- SOUND CONTROL - KIRI ATAS -->
|
|
||||||
<div class="sound-control-container">
|
<div class="sound-control-container">
|
||||||
<!-- Backdrop Overlay (Hidden by default) -->
|
|
||||||
<div class="volume-backdrop" id="volume-backdrop"></div>
|
<div class="volume-backdrop" id="volume-backdrop"></div>
|
||||||
|
|
||||||
<!-- Main Sound Button - HAPUS class "icon-btn" -->
|
|
||||||
<button class="btn-sound-main" id="btn-sound-main" title="Sound Settings">
|
<button class="btn-sound-main" id="btn-sound-main" title="Sound Settings">
|
||||||
<!-- Icon Full Sound (avg ≥ 60%) -->
|
|
||||||
<svg
|
<svg
|
||||||
class="sound-full"
|
class="sound-full"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -45,8 +38,6 @@
|
|||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
<path d="M15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14" />
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Icon Medium Sound (avg ≥ 30% && < 60%) -->
|
|
||||||
<svg
|
<svg
|
||||||
class="sound-medium"
|
class="sound-medium"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -58,8 +49,6 @@
|
|||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Icon Low Sound (avg > 0% && < 30%) -->
|
|
||||||
<svg
|
<svg
|
||||||
class="sound-low"
|
class="sound-low"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -70,8 +59,6 @@
|
|||||||
>
|
>
|
||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Icon Muted (all volumes = 0%) -->
|
|
||||||
<svg
|
<svg
|
||||||
class="sound-muted"
|
class="sound-muted"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -85,10 +72,7 @@
|
|||||||
<line x1="17" y1="9" x2="23" y2="15" />
|
<line x1="17" y1="9" x2="23" y2="15" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Volume Sliders Panel (Hidden by default) -->
|
|
||||||
<div class="volume-panel" id="volume-panel">
|
<div class="volume-panel" id="volume-panel">
|
||||||
<!-- Music Volume -->
|
|
||||||
<div class="volume-item">
|
<div class="volume-item">
|
||||||
<div class="volume-header">
|
<div class="volume-header">
|
||||||
<svg
|
<svg
|
||||||
@ -112,8 +96,6 @@
|
|||||||
value="25"
|
value="25"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pop SFX Volume -->
|
|
||||||
<div class="volume-item">
|
<div class="volume-item">
|
||||||
<div class="volume-header">
|
<div class="volume-header">
|
||||||
<svg
|
<svg
|
||||||
@ -138,8 +120,6 @@
|
|||||||
value="90"
|
value="90"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Merge SFX Volume -->
|
|
||||||
<div class="volume-item">
|
<div class="volume-item">
|
||||||
<div class="volume-header">
|
<div class="volume-header">
|
||||||
<svg
|
<svg
|
||||||
@ -165,8 +145,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top Right Controls -->
|
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
<button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play">
|
<button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play">
|
||||||
<svg
|
<svg
|
||||||
@ -191,10 +169,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Container -->
|
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
<!-- Header: Title + Scores -->
|
|
||||||
<div class="game-header">
|
<div class="game-header">
|
||||||
<h1>2048</h1>
|
<h1>2048</h1>
|
||||||
<div class="score-container">
|
<div class="score-container">
|
||||||
@ -208,12 +183,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Board -->
|
|
||||||
<div id="board"></div>
|
<div id="board"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tutorial Modal -->
|
|
||||||
<div class="tutorial-overlay" id="tutorial-overlay" style="display: none">
|
<div class="tutorial-overlay" id="tutorial-overlay" style="display: none">
|
||||||
<div class="tutorial-modal">
|
<div class="tutorial-modal">
|
||||||
<button class="modal-close" id="close-tutorial">
|
<button class="modal-close" id="close-tutorial">
|
||||||
@ -222,11 +193,8 @@
|
|||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h2 class="tutorial-title">How to Play</h2>
|
<h2 class="tutorial-title">How to Play</h2>
|
||||||
|
|
||||||
<div class="tutorial-content">
|
<div class="tutorial-content">
|
||||||
<!-- PC Controls -->
|
|
||||||
<div class="tutorial-section pc-controls">
|
<div class="tutorial-section pc-controls">
|
||||||
<h3>🖥️ PC Controls</h3>
|
<h3>🖥️ PC Controls</h3>
|
||||||
<div class="keys-container">
|
<div class="keys-container">
|
||||||
@ -255,8 +223,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Controls -->
|
|
||||||
<div class="tutorial-section mobile-controls">
|
<div class="tutorial-section mobile-controls">
|
||||||
<h3>📱 Mobile Controls</h3>
|
<h3>📱 Mobile Controls</h3>
|
||||||
<div class="swipe-demo">
|
<div class="swipe-demo">
|
||||||
@ -274,8 +240,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Over Modal -->
|
|
||||||
<div class="game-over-overlay" id="game-over-overlay" style="display: none">
|
<div class="game-over-overlay" id="game-over-overlay" style="display: none">
|
||||||
<div class="game-over-modal">
|
<div class="game-over-modal">
|
||||||
<button class="game-over-close" id="game-over-close">
|
<button class="game-over-close" id="game-over-close">
|
||||||
@ -284,24 +248,19 @@
|
|||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="game-over-title">No More Moves!</div>
|
<div class="game-over-title">No More Moves!</div>
|
||||||
<div class="game-over-subtitle">Game Over</div>
|
<div class="game-over-subtitle">Game Over</div>
|
||||||
|
|
||||||
<div class="game-over-score">
|
<div class="game-over-score">
|
||||||
<div class="game-over-score-label">Your Score</div>
|
<div class="game-over-score-label">Your Score</div>
|
||||||
<div class="game-over-score-value" id="final-score">0</div>
|
<div class="game-over-score-value" id="final-score">0</div>
|
||||||
|
|
||||||
<div class="new-high-score" id="new-high-score-badge" style="display: none">
|
<div class="new-high-score" id="new-high-score-badge" style="display: none">
|
||||||
🏆 New High Score!
|
🏆 New High Score!
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="high-score-display" id="high-score-display" style="display: none">
|
<div class="high-score-display" id="high-score-display" style="display: none">
|
||||||
<div class="high-score-label">High Score</div>
|
<div class="high-score-label">High Score</div>
|
||||||
<div class="high-score-value" id="modal-high-score">0</div>
|
<div class="high-score-value" id="modal-high-score">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="game-over-buttons">
|
<div class="game-over-buttons">
|
||||||
<button class="btn-game-icon btn-restart-game" id="btn-play-again" title="Restart Game">
|
<button class="btn-game-icon btn-restart-game" id="btn-play-again" title="Restart Game">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@ -318,7 +277,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="Score_Request.js"></script>
|
<script src="Score_Request.js"></script>
|
||||||
<script src="2048.js"></script>
|
<script src="2048.js"></script>
|
||||||
<script src="2048_Audio.js"></script>
|
<script src="2048_Audio.js"></script>
|
||||||
|
|||||||
12
2048.js
12
2048.js
@ -1,42 +1,30 @@
|
|||||||
/* ------------------------
|
|
||||||
1. GAME STATE & VARIABLES
|
|
||||||
------------------------ */
|
|
||||||
let board = [];
|
let board = [];
|
||||||
let currentScore = 0;
|
let currentScore = 0;
|
||||||
|
|
||||||
// Ambil username dari sessionStorage (Cek apakah ada user login)
|
|
||||||
const loggedInUser = sessionStorage.getItem("loggedInUser");
|
const loggedInUser = sessionStorage.getItem("loggedInUser");
|
||||||
|
|
||||||
// Tentukan user saat ini (jika tidak ada login, pakai "guest")
|
|
||||||
const currentUser = loggedInUser || "guest";
|
const currentUser = loggedInUser || "guest";
|
||||||
|
|
||||||
// Buat nama kunci unik untuk penyimpanan
|
|
||||||
const storageKey = 'highScore2048_' + currentUser;
|
const storageKey = 'highScore2048_' + currentUser;
|
||||||
|
|
||||||
// --- PERBAIKAN DI SINI ---
|
|
||||||
let highScore = 0;
|
let highScore = 0;
|
||||||
|
|
||||||
if (loggedInUser) {
|
if (loggedInUser) {
|
||||||
// JIKA SUDAH LOGIN: Ambil High Score dari memori localStorage
|
|
||||||
highScore = parseInt(localStorage.getItem(storageKey)) || 0;
|
highScore = parseInt(localStorage.getItem(storageKey)) || 0;
|
||||||
} else {
|
} else {
|
||||||
// JIKA BELUM LOGIN (GUEST): Selalu mulai High Score dari 0 saat refresh
|
|
||||||
highScore = 0;
|
highScore = 0;
|
||||||
}
|
}
|
||||||
// -------------------------
|
|
||||||
|
|
||||||
let lastMoveDir = null;
|
let lastMoveDir = null;
|
||||||
let isMoving = false;
|
let isMoving = false;
|
||||||
let mergesInCurrentMove = 0;
|
let mergesInCurrentMove = 0;
|
||||||
|
|
||||||
// Sound State (baca dari localStorage atau default ON)
|
|
||||||
let soundState = {
|
let soundState = {
|
||||||
bg: localStorage.getItem('sound_bg') !== 'false',
|
bg: localStorage.getItem('sound_bg') !== 'false',
|
||||||
pop: localStorage.getItem('sound_pop') !== 'false',
|
pop: localStorage.getItem('sound_pop') !== 'false',
|
||||||
merge: localStorage.getItem('sound_merge') !== 'false'
|
merge: localStorage.getItem('sound_merge') !== 'false'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Volume State (0-100 for each sound)
|
|
||||||
let volumeState = {
|
let volumeState = {
|
||||||
music: parseInt(localStorage.getItem('vol_music')) || 25,
|
music: parseInt(localStorage.getItem('vol_music')) || 25,
|
||||||
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
|
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
|
||||||
|
|||||||
102
2048_Audio.js
102
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 = {
|
const audio = {
|
||||||
bg: new Audio("Background_Music.mp3"), // Background music
|
bg: new Audio("Background_Music.mp3"),
|
||||||
pop: new Audio("Pop.mp3"), // Sound saat tile spawn
|
pop: new Audio("Pop.mp3"),
|
||||||
merge: new Audio("Merge.mp3") // Sound saat tile merge
|
merge: new Audio("Merge.mp3")
|
||||||
};
|
};
|
||||||
|
|
||||||
audio.bg.loop = true; // Background music loop terus
|
audio.bg.loop = true;
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
UPDATE VOLUME DARI STATE & SLIDERS
|
|
||||||
========================================== */
|
|
||||||
function updateAudioVolumes() {
|
function updateAudioVolumes() {
|
||||||
// Formula: volume = enabled ? (slider / 100) : 0
|
|
||||||
audio.bg.volume = soundState.bg ? (volumeState.music / 100) : 0;
|
audio.bg.volume = soundState.bg ? (volumeState.music / 100) : 0;
|
||||||
audio.pop.volume = soundState.pop ? (volumeState.pop / 100) : 0;
|
audio.pop.volume = soundState.pop ? (volumeState.pop / 100) : 0;
|
||||||
audio.merge.volume = soundState.merge ? (volumeState.merge / 100) : 0;
|
audio.merge.volume = soundState.merge ? (volumeState.merge / 100) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
PLAY BACKGROUND MUSIC (dengan unlock)
|
|
||||||
========================================== */
|
|
||||||
function tryPlayBg() {
|
function tryPlayBg() {
|
||||||
if (!soundState.bg || volumeState.music === 0) return;
|
if (!soundState.bg || volumeState.music === 0) return;
|
||||||
|
|
||||||
// Coba play, kalau di-block browser (autoplay policy)
|
|
||||||
audio.bg.play().catch(() => {
|
audio.bg.play().catch(() => {
|
||||||
// Setup unlock: tunggu user interaction
|
|
||||||
const unlock = () => {
|
const unlock = () => {
|
||||||
if (soundState.bg && volumeState.music > 0) audio.bg.play().catch(()=>{});
|
if (soundState.bg && volumeState.music > 0) audio.bg.play().catch(()=>{});
|
||||||
window.removeEventListener("keydown", unlock);
|
window.removeEventListener("keydown", unlock);
|
||||||
@ -48,24 +26,17 @@ function tryPlayBg() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
PLAY SOUND (dengan mute check)
|
|
||||||
========================================== */
|
|
||||||
function playSound(soundObj) {
|
function playSound(soundObj) {
|
||||||
try {
|
try {
|
||||||
// Guard: cek mute state sebelum play
|
|
||||||
if (soundObj === audio.pop && (!soundState.pop || volumeState.pop === 0)) return;
|
if (soundObj === audio.pop && (!soundState.pop || volumeState.pop === 0)) return;
|
||||||
if (soundObj === audio.merge && (!soundState.merge || volumeState.merge === 0)) return;
|
if (soundObj === audio.merge && (!soundState.merge || volumeState.merge === 0)) return;
|
||||||
if (soundObj === audio.bg && (!soundState.bg || volumeState.music === 0)) return;
|
if (soundObj === audio.bg && (!soundState.bg || volumeState.music === 0)) return;
|
||||||
|
|
||||||
soundObj.currentTime = 0; // Restart dari awal
|
soundObj.currentTime = 0;
|
||||||
soundObj.play().catch(() => {}); // Play with error handling
|
soundObj.play().catch(() => {});
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
INIT VOLUME SLIDERS - Setup Event Listeners
|
|
||||||
========================================== */
|
|
||||||
function initVolumeControl() {
|
function initVolumeControl() {
|
||||||
updateAudioVolumes();
|
updateAudioVolumes();
|
||||||
|
|
||||||
@ -73,33 +44,28 @@ function initVolumeControl() {
|
|||||||
const popSlider = document.getElementById('vol-pop');
|
const popSlider = document.getElementById('vol-pop');
|
||||||
const mergeSlider = document.getElementById('vol-merge');
|
const mergeSlider = document.getElementById('vol-merge');
|
||||||
|
|
||||||
/* --- MUSIC SLIDER --- */
|
|
||||||
if (musicSlider) {
|
if (musicSlider) {
|
||||||
// Load nilai dari localStorage
|
|
||||||
musicSlider.value = volumeState.music;
|
musicSlider.value = volumeState.music;
|
||||||
updateSliderFill(musicSlider, volumeState.music);
|
updateSliderFill(musicSlider, volumeState.music);
|
||||||
document.getElementById('vol-music-display').textContent = volumeState.music + '%';
|
document.getElementById('vol-music-display').textContent = volumeState.music + '%';
|
||||||
|
|
||||||
// Event listener saat slider digeser
|
|
||||||
musicSlider.addEventListener('input', (e) => {
|
musicSlider.addEventListener('input', (e) => {
|
||||||
const val = parseInt(e.target.value);
|
const val = parseInt(e.target.value);
|
||||||
volumeState.music = val; // Update state
|
volumeState.music = val;
|
||||||
audio.bg.volume = val / 100; // Set volume langsung
|
audio.bg.volume = val / 100;
|
||||||
localStorage.setItem('vol_music', val); // Save ke browser
|
localStorage.setItem('vol_music', val);
|
||||||
document.getElementById('vol-music-display').textContent = val + '%';
|
document.getElementById('vol-music-display').textContent = val + '%';
|
||||||
updateSliderFill(e.target, val); // Update visual fill
|
updateSliderFill(e.target, val);
|
||||||
updateMainSoundIcon(); // Update icon
|
updateMainSoundIcon();
|
||||||
|
|
||||||
// Auto play/pause background music
|
|
||||||
if (val > 0 && audio.bg.paused && soundState.bg) {
|
if (val > 0 && audio.bg.paused && soundState.bg) {
|
||||||
tryPlayBg(); // Play kalau volume > 0
|
tryPlayBg();
|
||||||
} else if (val === 0) {
|
} else if (val === 0) {
|
||||||
audio.bg.pause(); // Pause kalau volume = 0 (muted)
|
audio.bg.pause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- POP SLIDER (sama seperti music) --- */
|
|
||||||
if (popSlider) {
|
if (popSlider) {
|
||||||
popSlider.value = volumeState.pop;
|
popSlider.value = volumeState.pop;
|
||||||
updateSliderFill(popSlider, volumeState.pop);
|
updateSliderFill(popSlider, volumeState.pop);
|
||||||
@ -116,7 +82,6 @@ function initVolumeControl() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- MERGE SLIDER (sama seperti music) --- */
|
|
||||||
if (mergeSlider) {
|
if (mergeSlider) {
|
||||||
mergeSlider.value = volumeState.merge;
|
mergeSlider.value = volumeState.merge;
|
||||||
updateSliderFill(mergeSlider, volumeState.merge);
|
updateSliderFill(mergeSlider, volumeState.merge);
|
||||||
@ -134,110 +99,83 @@ function initVolumeControl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateMainSoundIcon();
|
updateMainSoundIcon();
|
||||||
setupVolumePanelEvents(); // Setup panel interactions
|
setupVolumePanelEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
SETUP PANEL EVENTS - Open/Close Logic
|
|
||||||
==========================================
|
|
||||||
Ini yang nge-link dengan inputLocked di Controls
|
|
||||||
========================================== */
|
|
||||||
function setupVolumePanelEvents() {
|
function setupVolumePanelEvents() {
|
||||||
const btnSoundMain = document.getElementById('btn-sound-main');
|
const btnSoundMain = document.getElementById('btn-sound-main');
|
||||||
const volumePanel = document.getElementById('volume-panel');
|
const volumePanel = document.getElementById('volume-panel');
|
||||||
const volumeBackdrop = document.getElementById('volume-backdrop');
|
const volumeBackdrop = document.getElementById('volume-backdrop');
|
||||||
|
|
||||||
if (btnSoundMain && volumePanel) {
|
if (btnSoundMain && volumePanel) {
|
||||||
// Event: Klik tombol sound main
|
|
||||||
btnSoundMain.addEventListener('click', (e) => {
|
btnSoundMain.addEventListener('click', (e) => {
|
||||||
e.stopPropagation(); // Jangan trigger event di parent
|
e.stopPropagation();
|
||||||
const isActive = volumePanel.classList.contains('active');
|
const isActive = volumePanel.classList.contains('active');
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
// TUTUP PANEL
|
|
||||||
volumePanel.classList.remove('active');
|
volumePanel.classList.remove('active');
|
||||||
if (volumeBackdrop) volumeBackdrop.classList.remove('active');
|
if (volumeBackdrop) volumeBackdrop.classList.remove('active');
|
||||||
inputLocked = false; // UNLOCK - game input aktif lagi
|
inputLocked = false;
|
||||||
} else {
|
} else {
|
||||||
// BUKA PANEL
|
|
||||||
volumePanel.classList.add('active');
|
volumePanel.classList.add('active');
|
||||||
if (volumeBackdrop) volumeBackdrop.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', () => {
|
volumeBackdrop.addEventListener('click', () => {
|
||||||
volumePanel.classList.remove('active');
|
volumePanel.classList.remove('active');
|
||||||
volumeBackdrop.classList.remove('active');
|
volumeBackdrop.classList.remove('active');
|
||||||
inputLocked = false; // UNLOCK
|
inputLocked = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event: Klik di luar panel (desktop)
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!volumePanel.contains(e.target) &&
|
if (!volumePanel.contains(e.target) &&
|
||||||
!btnSoundMain.contains(e.target) &&
|
!btnSoundMain.contains(e.target) &&
|
||||||
(!volumeBackdrop || !volumeBackdrop.contains(e.target))) {
|
(!volumeBackdrop || !volumeBackdrop.contains(e.target))) {
|
||||||
volumePanel.classList.remove('active');
|
volumePanel.classList.remove('active');
|
||||||
if (volumeBackdrop) volumeBackdrop.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) => {
|
volumePanel.addEventListener('click', (e) => {
|
||||||
e.stopPropagation(); // Prevent close
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
UPDATE VISUAL FILL SLIDER
|
|
||||||
========================================== */
|
|
||||||
function updateSliderFill(slider, value) {
|
function updateSliderFill(slider, value) {
|
||||||
// Set CSS custom property untuk animasi fill
|
|
||||||
// Dipakai di CSS: background: linear-gradient(...)
|
|
||||||
slider.style.setProperty('--value', value + '%');
|
slider.style.setProperty('--value', value + '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
UPDATE ICON DINAMIS - Sesuai Volume Level
|
|
||||||
========================================== */
|
|
||||||
function updateMainSoundIcon() {
|
function updateMainSoundIcon() {
|
||||||
const btnMain = document.getElementById('btn-sound-main');
|
const btnMain = document.getElementById('btn-sound-main');
|
||||||
if (!btnMain) return;
|
if (!btnMain) return;
|
||||||
|
|
||||||
// Ambil semua icon
|
|
||||||
const iconFull = btnMain.querySelector('.sound-full');
|
const iconFull = btnMain.querySelector('.sound-full');
|
||||||
const iconMedium = btnMain.querySelector('.sound-medium');
|
const iconMedium = btnMain.querySelector('.sound-medium');
|
||||||
const iconLow = btnMain.querySelector('.sound-low');
|
const iconLow = btnMain.querySelector('.sound-low');
|
||||||
const iconMuted = btnMain.querySelector('.sound-muted');
|
const iconMuted = btnMain.querySelector('.sound-muted');
|
||||||
|
|
||||||
// Hitung rata-rata volume dari 3 slider
|
|
||||||
const totalVolume = volumeState.music + volumeState.pop + volumeState.merge;
|
const totalVolume = volumeState.music + volumeState.pop + volumeState.merge;
|
||||||
const avgVolume = totalVolume / 3;
|
const avgVolume = totalVolume / 3;
|
||||||
|
|
||||||
// Hide semua icon dulu
|
|
||||||
if (iconFull) iconFull.style.display = 'none';
|
if (iconFull) iconFull.style.display = 'none';
|
||||||
if (iconMedium) iconMedium.style.display = 'none';
|
if (iconMedium) iconMedium.style.display = 'none';
|
||||||
if (iconLow) iconLow.style.display = 'none';
|
if (iconLow) iconLow.style.display = 'none';
|
||||||
if (iconMuted) iconMuted.style.display = 'none';
|
if (iconMuted) iconMuted.style.display = 'none';
|
||||||
|
|
||||||
// Show icon yang sesuai dengan level volume
|
|
||||||
if (totalVolume === 0) {
|
if (totalVolume === 0) {
|
||||||
// Semua muted
|
|
||||||
if (iconMuted) iconMuted.style.display = 'block';
|
if (iconMuted) iconMuted.style.display = 'block';
|
||||||
btnMain.classList.add('all-muted');
|
btnMain.classList.add('all-muted');
|
||||||
} else {
|
} else {
|
||||||
btnMain.classList.remove('all-muted');
|
btnMain.classList.remove('all-muted');
|
||||||
if (avgVolume >= 60) {
|
if (avgVolume >= 60) {
|
||||||
// Volume tinggi (≥60%)
|
|
||||||
if (iconFull) iconFull.style.display = 'block';
|
if (iconFull) iconFull.style.display = 'block';
|
||||||
} else if (avgVolume >= 30) {
|
} else if (avgVolume >= 30) {
|
||||||
// Volume sedang (30-59%)
|
|
||||||
if (iconMedium) iconMedium.style.display = 'block';
|
if (iconMedium) iconMedium.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
// Volume rendah (1-29%)
|
|
||||||
if (iconLow) iconLow.style.display = 'block';
|
if (iconLow) iconLow.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
let inputLocked = false;
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
KEYBOARD INPUT HANDLER
|
|
||||||
========================================== */
|
|
||||||
function handleKey(e) {
|
function handleKey(e) {
|
||||||
// Blokir input kalau:
|
|
||||||
// 1. Tile sedang bergerak (isMoving = true)
|
|
||||||
// 2. Panel volume sedang dibuka (inputLocked = true)
|
|
||||||
if (isMoving || inputLocked) return;
|
if (isMoving || inputLocked) return;
|
||||||
|
|
||||||
let moved = false;
|
let moved = false;
|
||||||
const k = e.key;
|
const k = e.key;
|
||||||
|
|
||||||
// Deteksi 4 arah: Arrow keys atau WASD
|
|
||||||
if (k === "ArrowLeft" || k === "a" || k === "A") {
|
if (k === "ArrowLeft" || k === "a" || k === "A") {
|
||||||
e.preventDefault(); // Cegah scroll halaman
|
e.preventDefault();
|
||||||
moved = moveLeft(); }
|
moved = moveLeft(); }
|
||||||
else if (k === "ArrowRight" || k === "d" || k === "D") {
|
else if (k === "ArrowRight" || k === "d" || k === "D") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -36,23 +19,19 @@ function handleKey(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
moved = moveDown(); }
|
moved = moveDown(); }
|
||||||
|
|
||||||
// Kalau tile berhasil bergerak
|
|
||||||
if (moved) {
|
if (moved) {
|
||||||
isMoving = true; // Lock input biar nggak spam
|
isMoving = true;
|
||||||
|
|
||||||
// Delay 100ms untuk animasi selesai
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const added = addNewTile(); // Spawn tile baru
|
const added = addNewTile();
|
||||||
|
|
||||||
// Cek game over: board penuh ATAU nggak bisa move
|
|
||||||
if (!added || !canMove()) {
|
if (!added || !canMove()) {
|
||||||
setTimeout(() => showGameOver(), 300);
|
setTimeout(() => showGameOver(), 300);
|
||||||
}
|
}
|
||||||
isMoving = false; // Unlock input
|
isMoving = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//Invalid move - Kasih feedback shake
|
|
||||||
const b = document.getElementById("board");
|
const b = document.getElementById("board");
|
||||||
if (b) {
|
if (b) {
|
||||||
b.classList.add("shake");
|
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 touchStartX = 0;
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
|
|
||||||
// Event 1: Catat posisi awal saat jari menyentuh layar
|
|
||||||
document.addEventListener("touchstart", function (e) {
|
document.addEventListener("touchstart", function (e) {
|
||||||
if (inputLocked) return; // Jangan catat swipe kalau panel volume aktif
|
if (inputLocked) return;
|
||||||
|
|
||||||
const t = e.touches[0];
|
const t = e.touches[0];
|
||||||
touchStartX = t.clientX; // Posisi X awal
|
touchStartX = t.clientX;
|
||||||
touchStartY = t.clientY; // Posisi Y awal
|
touchStartY = t.clientY;
|
||||||
}, { passive: true }); // Passive = performa lebih smooth
|
}, { passive: true });
|
||||||
|
|
||||||
// Event 2: Deteksi arah swipe saat jari diangkat
|
|
||||||
document.addEventListener("touchend", function (e) {
|
document.addEventListener("touchend", function (e) {
|
||||||
if (isMoving || inputLocked) return; // cek isMoving DAN inputLocked
|
if (isMoving || inputLocked) return;
|
||||||
|
|
||||||
const t = e.changedTouches[0];
|
const t = e.changedTouches[0];
|
||||||
|
|
||||||
// Hitung selisih posisi (delta)
|
const dx = t.clientX - touchStartX;
|
||||||
const dx = t.clientX - touchStartX; //Horizontal movement
|
const dy = t.clientY - touchStartY;
|
||||||
const dy = t.clientY - touchStartY; //Vertical movement
|
|
||||||
|
|
||||||
let moved = false;
|
let moved = false;
|
||||||
|
|
||||||
// Tentukan arah swipe berdasarkan delta terbesar
|
|
||||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
|
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) {
|
||||||
// Swipe HORIZONTAL (kiri/kanan)
|
if (dx > 0) moved = moveRight();
|
||||||
// Minimal 30px biar nggak terlalu sensitif
|
else moved = moveLeft();
|
||||||
if (dx > 0) moved = moveRight(); // swipe kanan
|
|
||||||
else moved = moveLeft(); // swipe kiri
|
|
||||||
|
|
||||||
} else if (Math.abs(dy) > 30) {
|
} else if (Math.abs(dy) > 30) {
|
||||||
// Swipe VERTICAL (atas/bawah)
|
if (dy > 0) moved = moveDown();
|
||||||
if (dy > 0) moved = moveDown(); // swipe bawah
|
else moved = moveUp();
|
||||||
else moved = moveUp(); // swipe atas
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logic sama seperti keyboard handler
|
|
||||||
if (moved) {
|
if (moved) {
|
||||||
isMoving = true;
|
isMoving = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -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() {
|
(function() {
|
||||||
// Ambil container untuk partikel
|
|
||||||
const container = document.getElementById('floating-particles');
|
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 particleColors = ['cyan', 'pink', 'purple', 'green', 'orange'];
|
||||||
const particleCount = 25; // Total partikel yang muncul
|
const particleCount = 25;
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
CREATE PARTICLE - Buat 1 partikel
|
|
||||||
========================================== */
|
|
||||||
function createParticle() {
|
function createParticle() {
|
||||||
// Buat element div untuk partikel
|
|
||||||
const particle = document.createElement('div');
|
const particle = document.createElement('div');
|
||||||
|
|
||||||
// Pilih warna random dari array
|
|
||||||
const randomColor = particleColors[Math.floor(Math.random() * particleColors.length)];
|
const randomColor = particleColors[Math.floor(Math.random() * particleColors.length)];
|
||||||
particle.className = `floating-particle ${randomColor}`;
|
particle.className = `floating-particle ${randomColor}`;
|
||||||
|
|
||||||
// POSISI HORIZONTAL RANDOM (0-100%)
|
|
||||||
const leftPos = Math.random() * 100;
|
const leftPos = Math.random() * 100;
|
||||||
particle.style.left = leftPos + '%';
|
particle.style.left = leftPos + '%';
|
||||||
|
|
||||||
// DRIFT HORIZONTAL (gerakan ke kiri/kanan saat naik)
|
|
||||||
// Range: -75px sampai +75px
|
|
||||||
const drift = (Math.random() - 0.5) * 150;
|
const drift = (Math.random() - 0.5) * 150;
|
||||||
particle.style.setProperty('--drift', drift + 'px');
|
particle.style.setProperty('--drift', drift + 'px');
|
||||||
|
|
||||||
// DURASI ANIMASI RANDOM (8-18 detik)
|
|
||||||
// Semakin lama = semakin smooth & dramatis
|
|
||||||
const duration = 8 + Math.random() * 10;
|
const duration = 8 + Math.random() * 10;
|
||||||
particle.style.animationDuration = duration + 's';
|
particle.style.animationDuration = duration + 's';
|
||||||
|
|
||||||
// DELAY RANDOM (0-5 detik)
|
|
||||||
// Biar nggak semua partikel muncul bareng (staggered)
|
|
||||||
const delay = Math.random() * 5;
|
const delay = Math.random() * 5;
|
||||||
particle.style.animationDelay = delay + 's';
|
particle.style.animationDelay = delay + 's';
|
||||||
|
|
||||||
// UKURAN RANDOM (6-14px)
|
|
||||||
const size = 6 + Math.random() * 8;
|
const size = 6 + Math.random() * 8;
|
||||||
particle.style.width = size + 'px';
|
particle.style.width = size + 'px';
|
||||||
particle.style.height = size + 'px';
|
particle.style.height = size + 'px';
|
||||||
|
|
||||||
// Append ke container
|
|
||||||
container.appendChild(particle);
|
container.appendChild(particle);
|
||||||
|
|
||||||
// ♻️ SELF-RECYCLING SYSTEM
|
|
||||||
// Setelah animasi selesai → hapus & buat baru
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
particle.remove(); // Hapus partikel lama
|
particle.remove();
|
||||||
createParticle(); // Buat partikel baru (infinite loop)
|
createParticle();
|
||||||
}, (duration + delay) * 1000); // Total waktu = duration + delay
|
}, (duration + delay) * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
INITIALIZE - Buat semua partikel awal
|
|
||||||
========================================== */
|
|
||||||
// Loop buat 25 partikel
|
|
||||||
for (let i = 0; i < particleCount; i++) {
|
for (let i = 0; i < particleCount; i++) {
|
||||||
// Stagger creation: jeda 200ms per partikel
|
|
||||||
// Biar nggak semua muncul sekaligus (smooth)
|
|
||||||
setTimeout(() => createParticle(), i * 200);
|
setTimeout(() => createParticle(), i * 200);
|
||||||
}
|
}
|
||||||
})(); // IIFE (Immediately Invoked Function Expression) - langsung jalan
|
})();
|
||||||
121
2048_Logic.js
121
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() {
|
function addNewTile() {
|
||||||
// Step 1: Cari semua cell kosong (value = 0)
|
|
||||||
const empty = [];
|
const empty = [];
|
||||||
for (let r = 0; r < 4; r++) {
|
for (let r = 0; r < 4; r++) {
|
||||||
for (let c = 0; c < 4; c++) {
|
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;
|
if (empty.length === 0) return false;
|
||||||
|
|
||||||
// Step 2: Pilih 1 posisi random dari cell kosong
|
|
||||||
const spot = empty[Math.floor(Math.random() * empty.length)];
|
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}`);
|
const tile = document.getElementById(`${spot.r}-${spot.c}`);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
tile.classList.add("new"); // Trigger animasi pop
|
tile.classList.add("new");
|
||||||
playSound(audio.pop); // Sound effect
|
playSound(audio.pop);
|
||||||
setTimeout(() => tile.classList.remove("new"), 300);
|
setTimeout(() => tile.classList.remove("new"), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTile(spot.r, spot.c, 2); // Update visual tile
|
updateTile(spot.r, spot.c, 2);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
HELPER: FILTER ANGKA NOL
|
|
||||||
========================================== */
|
|
||||||
function filterZero(row) {
|
function filterZero(row) {
|
||||||
// Buang semua 0 dari array
|
|
||||||
// Contoh: [2, 0, 2, 0] → [2, 2]
|
|
||||||
return row.filter(n => n !== 0);
|
return row.filter(n => n !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
ALGORITMA SLIDE & MERGE (CORE!)
|
|
||||||
==========================================
|
|
||||||
Ini yang bikin 2+2=4, 4+4=8, dst
|
|
||||||
========================================== */
|
|
||||||
function slide(row) {
|
function slide(row) {
|
||||||
// Step 1: Filter zero - kumpulkan tile
|
|
||||||
// [2, 0, 2, 4] → [2, 2, 4]
|
|
||||||
row = filterZero(row);
|
row = filterZero(row);
|
||||||
|
|
||||||
let mergedThisMove = false;
|
let mergedThisMove = false;
|
||||||
let mergedPositions = []; // Posisi tile yang di-merge (untuk animasi)
|
let mergedPositions = [];
|
||||||
let mergeCount = 0; // Hitung berapa kali merge (untuk combo)
|
let mergeCount = 0;
|
||||||
|
|
||||||
// Step 2: Loop cek tile sebelahan
|
|
||||||
for (let i = 0; i < row.length - 1; i++) {
|
for (let i = 0; i < row.length - 1; i++) {
|
||||||
if (row[i] === row[i + 1]) {
|
if (row[i] === row[i + 1]) {
|
||||||
// MERGE! Tile sama ketemu
|
row[i] = row[i] * 2;
|
||||||
row[i] = row[i] * 2; // Tile pertama jadi 2x lipat
|
|
||||||
|
|
||||||
playSound(audio.merge); // Sound effect merge
|
playSound(audio.merge);
|
||||||
|
|
||||||
// Haptic feedback (getaran) di mobile
|
|
||||||
if (navigator.vibrate) {
|
if (navigator.vibrate) {
|
||||||
navigator.vibrate([80, 20, 80]);
|
navigator.vibrate([80, 20, 80]);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentScore += row[i]; // Tambah score
|
currentScore += row[i];
|
||||||
row[i + 1] = 0; // Tile kedua hilang (jadi 0)
|
row[i + 1] = 0;
|
||||||
|
|
||||||
mergedThisMove = true;
|
mergedThisMove = true;
|
||||||
mergedPositions.push(i); // Simpan posisi untuk animasi
|
mergedPositions.push(i);
|
||||||
mergeCount++;
|
mergeCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Filter zero lagi & padding
|
|
||||||
// [4, 0, 4] → [4, 4] → [4, 4, 0, 0]
|
|
||||||
row = filterZero(row);
|
row = filterZero(row);
|
||||||
while (row.length < 4) row.push(0); // Padding 0 di kanan
|
while (row.length < 4) row.push(0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
row,
|
row,
|
||||||
@ -97,42 +62,19 @@ function slide(row) {
|
|||||||
mergeCount };
|
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() {
|
function moveLeft() {
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let mergedCells = []; // Track cell yang di-merge
|
let mergedCells = [];
|
||||||
mergesInCurrentMove = 0; // Reset combo counter
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
// Loop setiap baris (horizontal)
|
|
||||||
for (let r = 0; r < 4; r++) {
|
for (let r = 0; r < 4; r++) {
|
||||||
const { row: newRow, mergedPositions, mergeCount } = slide(board[r]);
|
const { row: newRow, mergedPositions, mergeCount } = slide(board[r]);
|
||||||
|
|
||||||
// Cek apakah row berubah
|
|
||||||
if (!arraysEqual(newRow, board[r])) moved = true;
|
if (!arraysEqual(newRow, board[r])) moved = true;
|
||||||
board[r] = newRow;
|
board[r] = newRow;
|
||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
// Simpan posisi cell yang merge untuk combo effect
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
mergedPositions.forEach(c => {
|
mergedPositions.forEach(c => {
|
||||||
mergedCells.push({ r, c });
|
mergedCells.push({ r, c });
|
||||||
@ -140,7 +82,6 @@ function moveLeft() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kalau ada pergerakan, update visual + effect
|
|
||||||
if (moved) {
|
if (moved) {
|
||||||
refreshBoard();
|
refreshBoard();
|
||||||
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
triggerComboEffect(mergedCells, mergesInCurrentMove);
|
||||||
@ -148,28 +89,24 @@ function moveLeft() {
|
|||||||
return moved;
|
return moved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MOVE RIGHT - Geser ke kanan */
|
|
||||||
function moveRight() {
|
function moveRight() {
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let mergedCells = [];
|
let mergedCells = [];
|
||||||
mergesInCurrentMove = 0;
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
// TRICK: Reverse → slide → reverse lagi
|
|
||||||
// Supaya bisa pakai logic slide yang sama
|
|
||||||
for (let r = 0; r < 4; r++) {
|
for (let r = 0; r < 4; r++) {
|
||||||
let reversed = [...board[r]].reverse();
|
let reversed = [...board[r]].reverse();
|
||||||
const { row: slid, mergedPositions, mergeCount } = slide(reversed);
|
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;
|
if (!arraysEqual(newRow, board[r])) moved = true;
|
||||||
board[r] = newRow;
|
board[r] = newRow;
|
||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
// Convert posisi merge ke koordinat asli (dari kanan)
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
mergedPositions.forEach(pos => {
|
mergedPositions.forEach(pos => {
|
||||||
const c = 3 - pos; // Mirror position
|
const c = 3 - pos;
|
||||||
mergedCells.push({ r, c });
|
mergedCells.push({ r, c });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -182,19 +119,15 @@ function moveRight() {
|
|||||||
return moved;
|
return moved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MOVE UP - Geser ke atas */
|
|
||||||
function moveUp() {
|
function moveUp() {
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let mergedCells = [];
|
let mergedCells = [];
|
||||||
mergesInCurrentMove = 0;
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
// Loop setiap kolom (vertical)
|
|
||||||
for (let c = 0; c < 4; c++) {
|
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 col = [board[0][c], board[1][c], board[2][c], board[3][c]];
|
||||||
const { row: newCol, mergedPositions, mergeCount } = slide(col);
|
const { row: newCol, mergedPositions, mergeCount } = slide(col);
|
||||||
|
|
||||||
// Update board per row
|
|
||||||
for (let r = 0; r < 4; r++) {
|
for (let r = 0; r < 4; r++) {
|
||||||
if (board[r][c] !== newCol[r]) moved = true;
|
if (board[r][c] !== newCol[r]) moved = true;
|
||||||
board[r][c] = newCol[r];
|
board[r][c] = newCol[r];
|
||||||
@ -216,17 +149,15 @@ function moveUp() {
|
|||||||
return moved;
|
return moved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MOVE DOWN - Geser ke bawah */
|
|
||||||
function moveDown() {
|
function moveDown() {
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let mergedCells = [];
|
let mergedCells = [];
|
||||||
mergesInCurrentMove = 0;
|
mergesInCurrentMove = 0;
|
||||||
|
|
||||||
for (let c = 0; c < 4; c++) {
|
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 col = [board[3][c], board[2][c], board[1][c], board[0][c]];
|
||||||
const { row: slid, mergedPositions, mergeCount } = slide(col);
|
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++) {
|
for (let r = 0; r < 4; r++) {
|
||||||
if (board[r][c] !== newCol[r]) moved = true;
|
if (board[r][c] !== newCol[r]) moved = true;
|
||||||
@ -235,10 +166,9 @@ function moveDown() {
|
|||||||
|
|
||||||
mergesInCurrentMove += mergeCount;
|
mergesInCurrentMove += mergeCount;
|
||||||
|
|
||||||
// Convert posisi merge ke koordinat asli (dari bawah)
|
|
||||||
if (mergedPositions && mergedPositions.length > 0) {
|
if (mergedPositions && mergedPositions.length > 0) {
|
||||||
mergedPositions.forEach(pos => {
|
mergedPositions.forEach(pos => {
|
||||||
const r = 3 - pos; // Mirror position
|
const r = 3 - pos;
|
||||||
mergedCells.push({ r, c });
|
mergedCells.push({ r, c });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -251,27 +181,22 @@ function moveDown() {
|
|||||||
return moved;
|
return moved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
CEK GAME OVER
|
|
||||||
========================================== */
|
|
||||||
function canMove() {
|
function canMove() {
|
||||||
// Kondisi 1: Ada cell kosong?
|
|
||||||
for (let r = 0; r < 4; r++) {
|
for (let r = 0; r < 4; r++) {
|
||||||
for (let c = 0; c < 4; c++) {
|
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 r = 0; r < 4; r++) {
|
||||||
for (let c = 0; c < 4; c++) {
|
for (let c = 0; c < 4; c++) {
|
||||||
const current = board[r][c];
|
const current = board[r][c];
|
||||||
//cek kanan
|
|
||||||
if (c < 3 && board[r][c + 1] === current) return true;
|
if (c < 3 && board[r][c + 1] === current) return true;
|
||||||
//cek bawah
|
|
||||||
if (r < 3 && board[r + 1][c] === current) return true;
|
if (r < 3 && board[r + 1][c] === current) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Kalau kedua kondisi false → STUCK = Game Over
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
11
2048_Main.js
11
2048_Main.js
@ -1,6 +1,3 @@
|
|||||||
/* ------------------------
|
|
||||||
7. MAIN INITIALIZATION
|
|
||||||
------------------------ */
|
|
||||||
function restartGame() {
|
function restartGame() {
|
||||||
hideGameOver();
|
hideGameOver();
|
||||||
resetScore();
|
resetScore();
|
||||||
@ -23,9 +20,7 @@ function goHome() {
|
|||||||
window.location.href = "Homepage.html";
|
window.location.href = "Homepage.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Event Listeners Setup */
|
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
// Tutorial Modal
|
|
||||||
const btnTutorial = document.getElementById('btn-tutorial');
|
const btnTutorial = document.getElementById('btn-tutorial');
|
||||||
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
||||||
const closeTutorial = document.getElementById('close-tutorial');
|
const closeTutorial = document.getElementById('close-tutorial');
|
||||||
@ -36,7 +31,6 @@ function setupEventListeners() {
|
|||||||
if (e.target === tutorialOverlay) tutorialOverlay.style.display = 'none';
|
if (e.target === tutorialOverlay) tutorialOverlay.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restart & Game Over buttons
|
|
||||||
const btnRestart = document.getElementById('btn-restart');
|
const btnRestart = document.getElementById('btn-restart');
|
||||||
if (btnRestart) btnRestart.addEventListener('click', restartGame);
|
if (btnRestart) btnRestart.addEventListener('click', restartGame);
|
||||||
|
|
||||||
@ -54,7 +48,6 @@ function setupEventListeners() {
|
|||||||
if (e.target === this) hideGameOver();
|
if (e.target === this) hideGameOver();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sound Buttons (Mute Toggles)
|
|
||||||
const btnSoundBg = document.getElementById('btn-sound-bg');
|
const btnSoundBg = document.getElementById('btn-sound-bg');
|
||||||
const btnSoundPop = document.getElementById('btn-sound-pop');
|
const btnSoundPop = document.getElementById('btn-sound-pop');
|
||||||
const btnSoundMerge = document.getElementById('btn-sound-merge');
|
const btnSoundMerge = document.getElementById('btn-sound-merge');
|
||||||
@ -64,7 +57,6 @@ function setupEventListeners() {
|
|||||||
localStorage.setItem('sound_bg', soundState.bg);
|
localStorage.setItem('sound_bg', soundState.bg);
|
||||||
updateAudioVolumes();
|
updateAudioVolumes();
|
||||||
if(soundState.bg) tryPlayBg(); else audio.bg.pause();
|
if(soundState.bg) tryPlayBg(); else audio.bg.pause();
|
||||||
// Tambahkan logika update tombol UI jika ada (toggle class)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (btnSoundPop) btnSoundPop.addEventListener('click', () => {
|
if (btnSoundPop) btnSoundPop.addEventListener('click', () => {
|
||||||
@ -80,13 +72,12 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DOM Ready */
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
updateHighScoreDisplay();
|
updateHighScoreDisplay();
|
||||||
setupBoard();
|
setupBoard();
|
||||||
addNewTile();
|
addNewTile();
|
||||||
addNewTile();
|
addNewTile();
|
||||||
initVolumeControl(); // Starts audio logic
|
initVolumeControl();
|
||||||
tryPlayBg();
|
tryPlayBg();
|
||||||
document.addEventListener("keydown", handleKey);
|
document.addEventListener("keydown", handleKey);
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
|||||||
@ -215,7 +215,6 @@
|
|||||||
letter-spacing: -2px;
|
letter-spacing: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New High Score Badge - GOLD */
|
|
||||||
.new-high-score {
|
.new-high-score {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 165, 0, 0.2));
|
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 {
|
.high-score-display {
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
padding-top: 28px;
|
padding-top: 28px;
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
/* Body & Container */
|
|
||||||
body {
|
body {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
@ -10,7 +7,6 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.game-header {
|
.game-header {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@ -38,7 +34,6 @@
|
|||||||
font-size: clamp(20px, 2.8vw, 25px);
|
font-size: clamp(20px, 2.8vw, 25px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top Controls */
|
|
||||||
.top-controls {
|
.top-controls {
|
||||||
top: clamp(8px, 1.5vh, 14px);
|
top: clamp(8px, 1.5vh, 14px);
|
||||||
right: clamp(8px, 1.5vw, 14px);
|
right: clamp(8px, 1.5vw, 14px);
|
||||||
@ -72,7 +67,6 @@
|
|||||||
height: clamp(16px, 2.8vw, 22px);
|
height: clamp(16px, 2.8vw, 22px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Board */
|
|
||||||
#board {
|
#board {
|
||||||
max-width: min(90vmin, 100%);
|
max-width: min(90vmin, 100%);
|
||||||
padding: clamp(8px, 1.8vmin, 14px);
|
padding: clamp(8px, 1.8vmin, 14px);
|
||||||
@ -80,18 +74,15 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tiles */
|
|
||||||
.tile {
|
.tile {
|
||||||
font-size: clamp(18px, 3.2vmin, 28px);
|
font-size: clamp(18px, 3.2vmin, 28px);
|
||||||
border-radius: clamp(6px, 1.5vmin, 12px);
|
border-radius: clamp(6px, 1.5vmin, 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Touch Hint */
|
|
||||||
.touch-hint {
|
.touch-hint {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tutorial Modal */
|
|
||||||
.tutorial-modal {
|
.tutorial-modal {
|
||||||
padding: 35px 30px;
|
padding: 35px 30px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
@ -111,12 +102,10 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PC Controls - Hide on Mobile */
|
|
||||||
.pc-controls {
|
.pc-controls {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Controls - Show on Mobile */
|
|
||||||
.mobile-controls {
|
.mobile-controls {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
@ -168,7 +157,6 @@
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Game Over Modal */
|
|
||||||
.game-over-modal {
|
.game-over-modal {
|
||||||
padding: 40px 28px 35px;
|
padding: 40px 28px 35px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
@ -246,7 +234,6 @@
|
|||||||
height: clamp(28px, 5.5vw, 34px);
|
height: clamp(28px, 5.5vw, 34px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sound Control */
|
|
||||||
.sound-control-container {
|
.sound-control-container {
|
||||||
top: clamp(8px, 1.5vh, 14px);
|
top: clamp(8px, 1.5vh, 14px);
|
||||||
left: clamp(8px, 1.5vw, 14px);
|
left: clamp(8px, 1.5vw, 14px);
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sound Button Styling */
|
|
||||||
.btn-sound {
|
.btn-sound {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: clamp(36px, 6vw, 48px);
|
width: clamp(36px, 6vw, 48px);
|
||||||
@ -36,7 +35,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BG Music Button - Purple */
|
|
||||||
#btn-sound-bg {
|
#btn-sound-bg {
|
||||||
background: rgba(50, 0, 70, 0.85);
|
background: rgba(50, 0, 70, 0.85);
|
||||||
border-color: rgba(200, 100, 255, 0.45);
|
border-color: rgba(200, 100, 255, 0.45);
|
||||||
@ -59,7 +57,6 @@
|
|||||||
color: rgba(200, 100, 255, 1);
|
color: rgba(200, 100, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pop SFX Button - Cyan */
|
|
||||||
#btn-sound-pop {
|
#btn-sound-pop {
|
||||||
background: rgba(0, 40, 50, 0.85);
|
background: rgba(0, 40, 50, 0.85);
|
||||||
border-color: rgba(0, 234, 255, 0.45);
|
border-color: rgba(0, 234, 255, 0.45);
|
||||||
@ -82,7 +79,6 @@
|
|||||||
color: rgba(0, 234, 255, 1);
|
color: rgba(0, 234, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Merge SFX Button - Orange/Yellow */
|
|
||||||
#btn-sound-merge {
|
#btn-sound-merge {
|
||||||
background: rgba(60, 30, 0, 0.85);
|
background: rgba(60, 30, 0, 0.85);
|
||||||
border-color: rgba(255, 170, 0, 0.45);
|
border-color: rgba(255, 170, 0, 0.45);
|
||||||
@ -105,7 +101,6 @@
|
|||||||
color: rgba(255, 170, 0, 1);
|
color: rgba(255, 170, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover & Active States */
|
|
||||||
.btn-sound:hover {
|
.btn-sound:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
@ -118,7 +113,6 @@
|
|||||||
inset 0 2px 6px rgba(0, 0, 0, 0.25);
|
inset 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Muted State - Red with X */
|
|
||||||
.btn-sound.muted {
|
.btn-sound.muted {
|
||||||
background: rgba(60, 0, 10, 0.85) !important;
|
background: rgba(60, 0, 10, 0.85) !important;
|
||||||
border-color: rgba(255, 50, 50, 0.6) !important;
|
border-color: rgba(255, 50, 50, 0.6) !important;
|
||||||
@ -150,7 +144,6 @@
|
|||||||
color: rgba(255, 100, 100, 1) !important;
|
color: rgba(255, 100, 100, 1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon Transitions */
|
|
||||||
.btn-sound svg.sound-icon,
|
.btn-sound svg.sound-icon,
|
||||||
.btn-sound svg.mute-icon {
|
.btn-sound svg.mute-icon {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@ -160,7 +153,6 @@
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FORCE tombol sound untuk selalu visible */
|
|
||||||
.btn-sound-main {
|
.btn-sound-main {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
@ -237,7 +229,6 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Backdrop Overlay untuk Mobile */
|
|
||||||
.volume-backdrop {
|
.volume-backdrop {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -252,7 +243,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Sound Button */
|
|
||||||
.btn-sound-main {
|
.btn-sound-main {
|
||||||
width: clamp(40px, 6.5vw, 52px);
|
width: clamp(40px, 6.5vw, 52px);
|
||||||
height: clamp(40px, 6.5vw, 52px);
|
height: clamp(40px, 6.5vw, 52px);
|
||||||
@ -300,7 +290,6 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Muted State - Red */
|
|
||||||
.btn-sound-main.all-muted {
|
.btn-sound-main.all-muted {
|
||||||
background: rgba(60, 0, 10, 0.9) !important;
|
background: rgba(60, 0, 10, 0.9) !important;
|
||||||
border-color: rgba(255, 60, 60, 0.7) !important;
|
border-color: rgba(255, 60, 60, 0.7) !important;
|
||||||
@ -319,7 +308,6 @@
|
|||||||
border-color: rgba(255, 80, 80, 0.9) !important;
|
border-color: rgba(255, 80, 80, 0.9) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Volume Panel */
|
|
||||||
.volume-panel {
|
.volume-panel {
|
||||||
display: none;
|
display: none;
|
||||||
background: linear-gradient(145deg, rgba(20, 0, 40, 0.98), rgba(30, 0, 50, 0.98));
|
background: linear-gradient(145deg, rgba(20, 0, 40, 0.98), rgba(30, 0, 50, 0.98));
|
||||||
@ -351,7 +339,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Volume Item */
|
|
||||||
.volume-item {
|
.volume-item {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
@ -390,7 +377,6 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Volume Slider */
|
|
||||||
.volume-slider {
|
.volume-slider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
@ -453,7 +439,6 @@
|
|||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slider Fill Effect */
|
|
||||||
.volume-slider {
|
.volume-slider {
|
||||||
background: linear-gradient(to right,
|
background: linear-gradient(to right,
|
||||||
rgba(0, 234, 255, 0.3) 0%,
|
rgba(0, 234, 255, 0.3) 0%,
|
||||||
|
|||||||
@ -1,38 +1,32 @@
|
|||||||
function checkAndShowTutorial() {
|
function checkAndShowTutorial() {
|
||||||
// Ambil user yang sedang login (atau guest)
|
|
||||||
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
||||||
const tutorialKey = 'tutorialSeen_' + currentUser;
|
const tutorialKey = 'tutorialSeen_' + currentUser;
|
||||||
|
|
||||||
console.log(`[Tutorial Check] User: ${currentUser}`);
|
console.log(`[Tutorial Check] User: ${currentUser}`);
|
||||||
console.log(`[Tutorial Check] Key: ${tutorialKey}`);
|
console.log(`[Tutorial Check] Key: ${tutorialKey}`);
|
||||||
|
|
||||||
// Cek apakah tutorial sudah pernah dilihat
|
|
||||||
const hasSeenTutorial = localStorage.getItem(tutorialKey);
|
const hasSeenTutorial = localStorage.getItem(tutorialKey);
|
||||||
console.log(`[Tutorial Check] Status Seen: ${hasSeenTutorial}`);
|
console.log(`[Tutorial Check] Status Seen: ${hasSeenTutorial}`);
|
||||||
|
|
||||||
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
||||||
|
|
||||||
// Tampilkan tutorial jika belum pernah dilihat
|
|
||||||
if (!hasSeenTutorial && tutorialOverlay) {
|
if (!hasSeenTutorial && tutorialOverlay) {
|
||||||
tutorialOverlay.style.display = 'flex';
|
tutorialOverlay.style.display = 'flex';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jalankan saat halaman selesai dimuat
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
checkAndShowTutorial(); // Cek & tampilkan tutorial
|
checkAndShowTutorial();
|
||||||
|
|
||||||
const closeTutorialBtn = document.getElementById('close-tutorial');
|
const closeTutorialBtn = document.getElementById('close-tutorial');
|
||||||
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
const tutorialOverlay = document.getElementById('tutorial-overlay');
|
||||||
|
|
||||||
// Event klik tombol tutup tutorial
|
|
||||||
if (closeTutorialBtn) {
|
if (closeTutorialBtn) {
|
||||||
closeTutorialBtn.addEventListener('click', () => {
|
closeTutorialBtn.addEventListener('click', () => {
|
||||||
// Ambil user aktif saat ini
|
|
||||||
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
|
||||||
const tutorialKey = 'tutorialSeen_' + currentUser;
|
const tutorialKey = 'tutorialSeen_' + currentUser;
|
||||||
|
|
||||||
// Sembunyikan tutorial & simpan status
|
|
||||||
if (tutorialOverlay) tutorialOverlay.style.display = 'none';
|
if (tutorialOverlay) tutorialOverlay.style.display = 'none';
|
||||||
localStorage.setItem(tutorialKey, 'true');
|
localStorage.setItem(tutorialKey, 'true');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
/* ------------------------
|
|
||||||
4. UI MANAGER
|
|
||||||
------------------------ */
|
|
||||||
function setupBoard() {
|
function setupBoard() {
|
||||||
board = [];
|
board = [];
|
||||||
currentScore = 0;
|
currentScore = 0;
|
||||||
|
|||||||
@ -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) {
|
function triggerComboEffect(mergedCells, comboCount) {
|
||||||
// Guard: kalau nggak ada tile yang merge, skip
|
|
||||||
if (mergedCells.length === 0) return;
|
if (mergedCells.length === 0) return;
|
||||||
|
|
||||||
// Loop setiap tile yang di-merge
|
|
||||||
mergedCells.forEach(cell => {
|
mergedCells.forEach(cell => {
|
||||||
const tile = document.getElementById(`${cell.r}-${cell.c}`);
|
const tile = document.getElementById(`${cell.r}-${cell.c}`);
|
||||||
|
|
||||||
if (!tile) return;
|
if (!tile) return;
|
||||||
|
|
||||||
// Efek 1: Animasi "merge" (scale + glow)
|
|
||||||
tile.classList.add('merge');
|
tile.classList.add('merge');
|
||||||
setTimeout(() => tile.classList.remove('merge'), 300);
|
setTimeout(() => tile.classList.remove('merge'), 300);
|
||||||
|
|
||||||
// Efek 2: Ledakan partikel
|
|
||||||
createParticleBurst(tile);
|
createParticleBurst(tile);
|
||||||
|
|
||||||
// Efek 3: Box shadow glow
|
|
||||||
tile.style.boxShadow = '0 0 40px currentColor';
|
tile.style.boxShadow = '0 0 40px currentColor';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tile.style.boxShadow = ''; // Reset setelah 300ms
|
tile.style.boxShadow = '';
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
// Efek 4: Score popup yang terbang ke atas
|
|
||||||
const rect = tile.getBoundingClientRect();
|
const rect = tile.getBoundingClientRect();
|
||||||
const centerX = rect.left + rect.width / 2;
|
const centerX = rect.left + rect.width / 2;
|
||||||
const centerY = rect.top + rect.height / 2;
|
const centerY = rect.top + rect.height / 2;
|
||||||
@ -44,54 +19,44 @@ function triggerComboEffect(mergedCells, comboCount) {
|
|||||||
createScorePopup(centerX, centerY, tileValue);
|
createScorePopup(centerX, centerY, tileValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Efek 5: Combo popup kalau merge ≥2 tile sekaligus
|
|
||||||
if (comboCount >= 2) {
|
if (comboCount >= 2) {
|
||||||
showComboPopup(comboCount);
|
showComboPopup(comboCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
COMBO POPUP - Text "COMBO x2!", "AMAZING x3!", dst
|
|
||||||
========================================== */
|
|
||||||
function showComboPopup(comboCount) {
|
function showComboPopup(comboCount) {
|
||||||
const board = document.getElementById('board');
|
const board = document.getElementById('board');
|
||||||
if (!board) return;
|
if (!board) return;
|
||||||
|
|
||||||
// Hitung posisi tengah board
|
|
||||||
const rect = board.getBoundingClientRect();
|
const rect = board.getBoundingClientRect();
|
||||||
const centerX = rect.left + rect.width / 2;
|
const centerX = rect.left + rect.width / 2;
|
||||||
const centerY = rect.top + rect.height / 2;
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
// Buat element popup
|
|
||||||
const popup = document.createElement('div');
|
const popup = document.createElement('div');
|
||||||
popup.className = 'combo-popup';
|
popup.className = 'combo-popup';
|
||||||
popup.style.left = centerX + 'px';
|
popup.style.left = centerX + 'px';
|
||||||
popup.style.top = centerY + 'px';
|
popup.style.top = centerY + 'px';
|
||||||
popup.style.position = 'fixed';
|
popup.style.position = 'fixed';
|
||||||
popup.style.fontWeight = '900';
|
popup.style.fontWeight = '900';
|
||||||
popup.style.pointerEvents = 'none'; // Nggak bisa diklik
|
popup.style.pointerEvents = 'none';
|
||||||
popup.style.zIndex = '9999'; // Di depan semua
|
popup.style.zIndex = '9999';
|
||||||
popup.style.transform = 'translate(-50%, -50%)'; // Center alignment
|
popup.style.transform = 'translate(-50%, -50%)';
|
||||||
popup.style.textTransform = 'uppercase';
|
popup.style.textTransform = 'uppercase';
|
||||||
popup.style.letterSpacing = '3px';
|
popup.style.letterSpacing = '3px';
|
||||||
|
|
||||||
// Styling berbeda sesuai combo level
|
|
||||||
if (comboCount === 2) {
|
if (comboCount === 2) {
|
||||||
// COMBO x2 - Hijau neon
|
|
||||||
popup.textContent = 'COMBO x2!';
|
popup.textContent = 'COMBO x2!';
|
||||||
popup.style.fontSize = '36px';
|
popup.style.fontSize = '36px';
|
||||||
popup.style.color = '#00ff99';
|
popup.style.color = '#00ff99';
|
||||||
popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)';
|
popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)';
|
||||||
|
|
||||||
} else if (comboCount === 3) {
|
} else if (comboCount === 3) {
|
||||||
// AMAZING x3 - Pink magenta
|
|
||||||
popup.textContent = 'AMAZING x3!';
|
popup.textContent = 'AMAZING x3!';
|
||||||
popup.style.fontSize = '42px';
|
popup.style.fontSize = '42px';
|
||||||
popup.style.color = '#ff00ff';
|
popup.style.color = '#ff00ff';
|
||||||
popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)';
|
popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)';
|
||||||
|
|
||||||
} else if (comboCount >= 4) {
|
} else if (comboCount >= 4) {
|
||||||
// PERFECT x4+ - Gold
|
|
||||||
popup.textContent = 'PERFECT x' + comboCount + '!';
|
popup.textContent = 'PERFECT x' + comboCount + '!';
|
||||||
popup.style.fontSize = '48px';
|
popup.style.fontSize = '48px';
|
||||||
popup.style.color = '#ffd700';
|
popup.style.color = '#ffd700';
|
||||||
@ -100,7 +65,6 @@ function showComboPopup(comboCount) {
|
|||||||
|
|
||||||
document.body.appendChild(popup);
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
// Animasi: muncul → bounce → hilang
|
|
||||||
popup.animate([
|
popup.animate([
|
||||||
{ transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', opacity: 0 },
|
{ 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 },
|
{ 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 }
|
{ transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)', opacity: 0 }
|
||||||
], {
|
], {
|
||||||
duration: 1200,
|
duration: 1200,
|
||||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' // Bounce effect
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
|
||||||
}).onfinish = () => popup.remove(); // Auto cleanup
|
}).onfinish = () => popup.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
PARTICLE BURST - Ledakan partikel dari tile
|
|
||||||
========================================== */
|
|
||||||
function createParticleBurst(tileElement) {
|
function createParticleBurst(tileElement) {
|
||||||
// Ambil posisi tengah tile
|
|
||||||
const rect = tileElement.getBoundingClientRect();
|
const rect = tileElement.getBoundingClientRect();
|
||||||
const centerX = rect.left + rect.width / 2;
|
const centerX = rect.left + rect.width / 2;
|
||||||
const centerY = rect.top + rect.height / 2;
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
// Ambil warna tile sesuai nilainya
|
|
||||||
const tileValue = parseInt(tileElement.textContent);
|
const tileValue = parseInt(tileElement.textContent);
|
||||||
const tileColor = getTileColor(tileValue);
|
const tileColor = getTileColor(tileValue);
|
||||||
|
|
||||||
// Jumlah partikel random (8-12)
|
|
||||||
const particleCount = 8 + Math.floor(Math.random() * 5);
|
const particleCount = 8 + Math.floor(Math.random() * 5);
|
||||||
|
|
||||||
// Buat partikel dalam lingkaran (360°)
|
|
||||||
for (let i = 0; i < particleCount; i++) {
|
for (let i = 0; i < particleCount; i++) {
|
||||||
const particle = document.createElement('div');
|
const particle = document.createElement('div');
|
||||||
particle.className = 'merge-particle';
|
particle.className = 'merge-particle';
|
||||||
particle.style.left = centerX + 'px';
|
particle.style.left = centerX + 'px';
|
||||||
particle.style.top = centerY + 'px';
|
particle.style.top = centerY + 'px';
|
||||||
particle.style.background = tileColor; // Warna sama dengan tile
|
particle.style.background = tileColor;
|
||||||
|
|
||||||
document.body.appendChild(particle);
|
document.body.appendChild(particle);
|
||||||
|
|
||||||
// Hitung sudut & kecepatan untuk ledakan melingkar
|
|
||||||
const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
|
const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
|
||||||
const velocity = 60 + Math.random() * 40; // 60-100px
|
const velocity = 60 + Math.random() * 40;
|
||||||
const tx = Math.cos(angle) * velocity; // Posisi X
|
const tx = Math.cos(angle) * velocity;
|
||||||
const ty = Math.sin(angle) * velocity; // Posisi Y
|
const ty = Math.sin(angle) * velocity;
|
||||||
|
|
||||||
// Animasi: meledak keluar sambil mengecil
|
|
||||||
particle.animate([
|
particle.animate([
|
||||||
{ transform: 'translate(0, 0) scale(1)', opacity: 1 },
|
{ transform: 'translate(0, 0) scale(1)', opacity: 1 },
|
||||||
{ transform: `translate(${tx}px, ${ty}px) scale(0)`, opacity: 0 }
|
{ transform: `translate(${tx}px, ${ty}px) scale(0)`, opacity: 0 }
|
||||||
], {
|
], {
|
||||||
duration: 500 + Math.random() * 200, // 500-700ms
|
duration: 500 + Math.random() * 200,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' // Smooth ease-out
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||||
}).onfinish = () => particle.remove(); // Auto cleanup
|
}).onfinish = () => particle.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
SCORE POPUP - Angka score yang terbang ke atas
|
|
||||||
========================================== */
|
|
||||||
function createScorePopup(x, y, score) {
|
function createScorePopup(x, y, score) {
|
||||||
const popup = document.createElement('div');
|
const popup = document.createElement('div');
|
||||||
popup.className = 'score-popup';
|
popup.className = 'score-popup';
|
||||||
popup.textContent = '+' + score; // Contoh: "+16", "+32"
|
popup.textContent = '+' + score;
|
||||||
popup.style.left = x + 'px';
|
popup.style.left = x + 'px';
|
||||||
popup.style.top = y + 'px';
|
popup.style.top = y + 'px';
|
||||||
popup.style.position = 'fixed';
|
popup.style.position = 'fixed';
|
||||||
popup.style.fontSize = '24px';
|
popup.style.fontSize = '24px';
|
||||||
popup.style.fontWeight = '900';
|
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.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)';
|
||||||
popup.style.pointerEvents = 'none';
|
popup.style.pointerEvents = 'none';
|
||||||
popup.style.zIndex = '9999';
|
popup.style.zIndex = '9999';
|
||||||
popup.style.transform = 'translate(-50%, -50%)';
|
popup.style.transform = 'translate(-50%, -50%)';
|
||||||
|
|
||||||
document.body.appendChild(popup);
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
// Animasi: terbang ke atas sambil fade out
|
|
||||||
popup.animate([
|
popup.animate([
|
||||||
{ transform: 'translate(-50%, -50%) scale(0.5)', opacity: 0 },
|
{ transform: 'translate(-50%, -50%) scale(0.5)', opacity: 0 },
|
||||||
{ transform: 'translate(-50%, -70px) scale(1.2)', opacity: 1, offset: 0.3 },
|
{ transform: 'translate(-50%, -70px) scale(1.2)', opacity: 1, offset: 0.3 },
|
||||||
{ transform: 'translate(-50%, -120px) scale(1)', opacity: 0 }
|
{ transform: 'translate(-50%, -120px) scale(1)', opacity: 0 }
|
||||||
], {
|
], {
|
||||||
duration: 1000,
|
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();
|
}).onfinish = () => popup.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
GET TILE COLOR - Warna sesuai nilai tile
|
|
||||||
==========================================
|
|
||||||
Dipakai untuk partikel biar warnanya match
|
|
||||||
========================================== */
|
|
||||||
function getTileColor(value) {
|
function getTileColor(value) {
|
||||||
const colors = {
|
const colors = {
|
||||||
2: '#00eaff', // Cyan
|
2: '#00eaff',
|
||||||
4: '#00ff99', // Green
|
4: '#00ff99',
|
||||||
8: '#ff00ff', // Magenta
|
8: '#ff00ff',
|
||||||
16: '#ff0066', // Pink
|
16: '#ff0066',
|
||||||
32: '#ffaa00', // Orange
|
32: '#ffaa00',
|
||||||
64: '#ff0000', // Red
|
64: '#ff0000',
|
||||||
128: '#5f00ff', // Purple
|
128: '#5f00ff',
|
||||||
256: '#00ffea', // Cyan bright
|
256: '#00ffea',
|
||||||
512: '#ff00aa', // Pink bright
|
512: '#ff00aa',
|
||||||
1024: '#00ffaa', // Green bright
|
1024: '#00ffaa',
|
||||||
2048: '#ffd700' // Gold (winning tile!)
|
2048: '#ffd700'
|
||||||
};
|
};
|
||||||
// Return warna sesuai value, default cyan kalau nggak ada
|
|
||||||
return colors[value] || '#00eaff';
|
return colors[value] || '#00eaff';
|
||||||
}
|
}
|
||||||
@ -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() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
DOM ELEMENTS - Cache semua element penting
|
|
||||||
========================================== */
|
|
||||||
const elements = {
|
const elements = {
|
||||||
logo: null,
|
logo: null,
|
||||||
authBtn: null,
|
authBtn: null,
|
||||||
@ -26,37 +12,29 @@
|
|||||||
logoutFailedOverlay: null
|
logoutFailedOverlay: null
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
INITIALIZE - Setup saat page load
|
|
||||||
========================================== */
|
|
||||||
function init() {
|
function init() {
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
// Kalau DOM belum ready, tunggu event
|
|
||||||
document.addEventListener('DOMContentLoaded', initAll);
|
document.addEventListener('DOMContentLoaded', initAll);
|
||||||
} else {
|
} else {
|
||||||
// Kalau sudah ready, langsung init
|
|
||||||
initAll();
|
initAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAll() {
|
function initAll() {
|
||||||
try {
|
try {
|
||||||
// Step 1: Cache semua DOM elements
|
|
||||||
cacheElements();
|
cacheElements();
|
||||||
|
|
||||||
// Step 2: Validasi element wajib ada
|
|
||||||
if (!validateElements()) {
|
if (!validateElements()) {
|
||||||
console.error('Some required elements are missing');
|
console.error('Some required elements are missing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Setup event listeners
|
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
|
||||||
// Step 4: Initialize smooth scroll
|
|
||||||
initSmoothScroll();
|
initSmoothScroll();
|
||||||
|
|
||||||
// Step 5: Update button LOGIN/LOGOUT sesuai status
|
|
||||||
updateAuthButton();
|
updateAuthButton();
|
||||||
|
|
||||||
console.log('✅ Homepage initialized successfully');
|
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() {
|
function cacheElements() {
|
||||||
elements.logo = document.querySelector('.logo');
|
elements.logo = document.querySelector('.logo');
|
||||||
elements.authBtn = document.getElementById('auth-button');
|
elements.authBtn = document.getElementById('auth-button');
|
||||||
@ -81,129 +54,89 @@
|
|||||||
elements.logoutFailedOverlay = document.getElementById('logout-failed-overlay');
|
elements.logoutFailedOverlay = document.getElementById('logout-failed-overlay');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
VALIDATE ELEMENTS - Cek element wajib ada
|
|
||||||
========================================== */
|
|
||||||
function validateElements() {
|
function validateElements() {
|
||||||
// Element yang HARUS ada: logo, authBtn, playBtn
|
|
||||||
const requiredElements = ['logo', 'authBtn', 'playBtn'];
|
const requiredElements = ['logo', 'authBtn', 'playBtn'];
|
||||||
const missingElements = requiredElements.filter(key => !elements[key]);
|
const missingElements = requiredElements.filter(key => !elements[key]);
|
||||||
|
|
||||||
if (missingElements.length > 0) {
|
if (missingElements.length > 0) {
|
||||||
console.warn('Missing elements:', missingElements);
|
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() {
|
function updateAuthButton() {
|
||||||
// Cek apakah user sudah login
|
|
||||||
// 3 cara cek: authToken, username, atau loggedInUser
|
|
||||||
const authToken = localStorage.getItem('authToken');
|
const authToken = localStorage.getItem('authToken');
|
||||||
const username = localStorage.getItem('username');
|
const username = localStorage.getItem('username');
|
||||||
const loggedInUser = sessionStorage.getItem('loggedInUser');
|
const loggedInUser = sessionStorage.getItem('loggedInUser');
|
||||||
|
|
||||||
if (authToken || username || loggedInUser) {
|
if (authToken || username || loggedInUser) {
|
||||||
// User SUDAH LOGIN → show LOGOUT button
|
|
||||||
elements.authBtn.textContent = 'LOGOUT';
|
elements.authBtn.textContent = 'LOGOUT';
|
||||||
elements.authBtn.classList.add('logout-mode');
|
elements.authBtn.classList.add('logout-mode');
|
||||||
} else {
|
} else {
|
||||||
// User BELUM LOGIN → show LOGIN button
|
|
||||||
elements.authBtn.textContent = 'LOGIN';
|
elements.authBtn.textContent = 'LOGIN';
|
||||||
elements.authBtn.classList.remove('logout-mode');
|
elements.authBtn.classList.remove('logout-mode');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
SETUP EVENT LISTENERS - Bind semua event
|
|
||||||
========================================== */
|
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
// Logo click → reload homepage
|
|
||||||
if (elements.logo) {
|
if (elements.logo) {
|
||||||
elements.logo.addEventListener('click', handleLogoClick);
|
elements.logo.addEventListener('click', handleLogoClick);
|
||||||
}
|
}
|
||||||
|
t
|
||||||
// Auth button → login/logout
|
|
||||||
if (elements.authBtn) {
|
if (elements.authBtn) {
|
||||||
elements.authBtn.addEventListener('click', handleAuthClick);
|
elements.authBtn.addEventListener('click', handleAuthClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play button → go to game
|
|
||||||
if (elements.playBtn) {
|
if (elements.playBtn) {
|
||||||
elements.playBtn.addEventListener('click', handlePlayClick);
|
elements.playBtn.addEventListener('click', handlePlayClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaderboard button
|
|
||||||
if (elements.leaderboardBtn) {
|
if (elements.leaderboardBtn) {
|
||||||
elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick);
|
elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcuts (P, L, B)
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
|
||||||
// Window events
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
EVENT HANDLERS
|
|
||||||
========================================== */
|
|
||||||
|
|
||||||
function handleLogoClick(e) {
|
function handleLogoClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = 'Homepage.html';
|
window.location.href = 'Homepage.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
HANDLE AUTH CLICK - Login atau Logout
|
|
||||||
========================================== */
|
|
||||||
function handleAuthClick(e) {
|
function handleAuthClick(e) {
|
||||||
// Cek status login dari localStorage & sessionStorage
|
|
||||||
const authToken = localStorage.getItem('authToken');
|
const authToken = localStorage.getItem('authToken');
|
||||||
const username = localStorage.getItem('username');
|
const username = localStorage.getItem('username');
|
||||||
const loggedInUser = sessionStorage.getItem('loggedInUser');
|
const loggedInUser = sessionStorage.getItem('loggedInUser');
|
||||||
|
|
||||||
if (authToken || username || loggedInUser) {
|
if (authToken || username || loggedInUser) {
|
||||||
// User sudah login → LOGOUT
|
|
||||||
handleLogout();
|
handleLogout();
|
||||||
} else {
|
} else {
|
||||||
// User belum login → ke halaman LOGIN
|
|
||||||
window.location.href = 'Login.html';
|
window.location.href = 'Login.html';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
HANDLE LOGOUT - Logout Logic dengan PHP
|
|
||||||
========================================== */
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
try {
|
try {
|
||||||
// Step 1: Panggil PHP untuk hapus session di server
|
|
||||||
const response = await fetch("http://localhost/Kelompok06_2048/Logout.php", {
|
const response = await fetch("http://localhost/Kelompok06_2048/Logout.php", {
|
||||||
method: "POST"
|
method: "POST"
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Step 2: Hapus token & username dari localStorage
|
|
||||||
localStorage.removeItem("authToken");
|
localStorage.removeItem("authToken");
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
|
|
||||||
// Step 3: Hapus juga dari sessionStorage
|
|
||||||
sessionStorage.removeItem('loggedInUser');
|
sessionStorage.removeItem('loggedInUser');
|
||||||
sessionStorage.removeItem('showTutorial');
|
sessionStorage.removeItem('showTutorial');
|
||||||
|
|
||||||
// Step 4: Show success modal
|
|
||||||
if (elements.logoutOverlay) {
|
if (elements.logoutOverlay) {
|
||||||
elements.logoutOverlay.style.display = 'flex';
|
elements.logoutOverlay.style.display = 'flex';
|
||||||
|
|
||||||
// Auto close setelah 2 detik & redirect
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
elements.logoutOverlay.style.display = 'none';
|
elements.logoutOverlay.style.display = 'none';
|
||||||
window.location.href = "Homepage.html";
|
window.location.href = "Homepage.html";
|
||||||
@ -213,17 +146,14 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout failed:', error);
|
console.error('Logout failed:', error);
|
||||||
|
|
||||||
// Tetap hapus data lokal meski server error
|
|
||||||
localStorage.removeItem("authToken");
|
localStorage.removeItem("authToken");
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
sessionStorage.removeItem('loggedInUser');
|
sessionStorage.removeItem('loggedInUser');
|
||||||
sessionStorage.removeItem('showTutorial');
|
sessionStorage.removeItem('showTutorial');
|
||||||
|
|
||||||
// Show error modal
|
|
||||||
if (elements.logoutFailedOverlay) {
|
if (elements.logoutFailedOverlay) {
|
||||||
elements.logoutFailedOverlay.style.display = 'flex';
|
elements.logoutFailedOverlay.style.display = 'flex';
|
||||||
|
|
||||||
// Auto close setelah 2.5 detik
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
elements.logoutFailedOverlay.style.display = 'none';
|
elements.logoutFailedOverlay.style.display = 'none';
|
||||||
window.location.href = "Homepage.html";
|
window.location.href = "Homepage.html";
|
||||||
@ -233,34 +163,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePlayClick(e) {
|
function handlePlayClick(e) {
|
||||||
// Allow default behavior (navigate ke 2048.html)
|
|
||||||
console.log('Starting game...');
|
console.log('Starting game...');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLeaderboardClick(e) {
|
function handleLeaderboardClick(e) {
|
||||||
// Allow default behavior (navigate ke leaderboard.html)
|
|
||||||
console.log('Opening leaderboard...');
|
console.log('Opening leaderboard...');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
KEYBOARD SHORTCUTS
|
|
||||||
========================================== */
|
|
||||||
function handleKeyPress(e) {
|
function handleKeyPress(e) {
|
||||||
// Press 'P' → Play game
|
|
||||||
if (e.key === 'p' || e.key === 'P') {
|
if (e.key === 'p' || e.key === 'P') {
|
||||||
if (elements.playBtn) {
|
if (elements.playBtn) {
|
||||||
elements.playBtn.click();
|
elements.playBtn.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Press 'L' → Login/Logout
|
|
||||||
if (e.key === 'l' || e.key === 'L') {
|
if (e.key === 'l' || e.key === 'L') {
|
||||||
if (elements.authBtn) {
|
if (elements.authBtn) {
|
||||||
elements.authBtn.click();
|
elements.authBtn.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Press 'B' → Leaderboard (Board)
|
|
||||||
if (e.key === 'b' || e.key === 'B') {
|
if (e.key === 'b' || e.key === 'B') {
|
||||||
if (elements.leaderboardBtn) {
|
if (elements.leaderboardBtn) {
|
||||||
elements.leaderboardBtn.click();
|
elements.leaderboardBtn.click();
|
||||||
@ -269,7 +191,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
// Handle responsive behavior (mobile, tablet, desktop)
|
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
|
|
||||||
if (width < 768) {
|
if (width < 768) {
|
||||||
@ -282,13 +203,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBeforeUnload(e) {
|
function handleBeforeUnload(e) {
|
||||||
// Cleanup sebelum page unload
|
|
||||||
console.log('Page unloading...');
|
console.log('Page unloading...');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
SMOOTH SCROLL - Untuk anchor links (#)
|
|
||||||
========================================== */
|
|
||||||
function initSmoothScroll() {
|
function initSmoothScroll() {
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
anchor.addEventListener('click', function(e) {
|
anchor.addEventListener('click', function(e) {
|
||||||
@ -308,9 +225,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
UTILITY FUNCTIONS
|
|
||||||
========================================== */
|
|
||||||
function checkBrowserSupport() {
|
function checkBrowserSupport() {
|
||||||
const features = {
|
const features = {
|
||||||
localStorage: typeof(Storage) !== 'undefined',
|
localStorage: typeof(Storage) !== 'undefined',
|
||||||
@ -326,9 +240,6 @@
|
|||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
PUBLIC API - Fungsi yang bisa dipanggil dari luar
|
|
||||||
========================================== */
|
|
||||||
window.Homepage = {
|
window.Homepage = {
|
||||||
init: initAll,
|
init: initAll,
|
||||||
isMobile: isMobile,
|
isMobile: isMobile,
|
||||||
@ -336,11 +247,7 @@
|
|||||||
updateAuthButton: updateAuthButton
|
updateAuthButton: updateAuthButton
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
CLEANUP - Remove semua event listener
|
|
||||||
========================================== */
|
|
||||||
window.cleanupHomepage = function() {
|
window.cleanupHomepage = function() {
|
||||||
// Remove event listeners (untuk prevent memory leak)
|
|
||||||
if (elements.logo) {
|
if (elements.logo) {
|
||||||
elements.logo.removeEventListener('click', handleLogoClick);
|
elements.logo.removeEventListener('click', handleLogoClick);
|
||||||
}
|
}
|
||||||
@ -364,7 +271,6 @@
|
|||||||
console.log('Homepage cleaned up');
|
console.log('Homepage cleaned up');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start initialization saat script load
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
})(); // IIFE - langsung execute
|
})();
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// Particles Animation
|
|
||||||
const particlesContainer = document.getElementById('particles');
|
const particlesContainer = document.getElementById('particles');
|
||||||
|
|
||||||
function createParticle() {
|
function createParticle() {
|
||||||
|
|||||||
@ -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 particlesContainer = document.getElementById('particles');
|
||||||
const particleCount = 150; // Total partikel yang dibuat
|
const particleCount = 150;
|
||||||
const particles = []; // Array untuk simpan semua partikel
|
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 {
|
class Particle {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Buat element DOM untuk partikel
|
|
||||||
this.element = document.createElement('div');
|
this.element = document.createElement('div');
|
||||||
this.element.className = 'particle';
|
this.element.className = 'particle';
|
||||||
this.reset(); // Initialize posisi & properti
|
this.reset(); // Initialize posisi & properti
|
||||||
particlesContainer.appendChild(this.element);
|
particlesContainer.appendChild(this.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
RESET - Set/reset properti partikel
|
|
||||||
========================================== */
|
|
||||||
reset() {
|
reset() {
|
||||||
// Posisi random di seluruh layar
|
|
||||||
this.x = Math.random() * window.innerWidth;
|
this.x = Math.random() * window.innerWidth;
|
||||||
this.y = Math.random() * window.innerHeight;
|
this.y = Math.random() * window.innerHeight;
|
||||||
|
|
||||||
// Kecepatan random (-0.6 sampai +0.6 pixel/frame)
|
|
||||||
this.vx = (Math.random() - 0.5) * 1.2;
|
this.vx = (Math.random() - 0.5) * 1.2;
|
||||||
this.vy = (Math.random() - 0.5) * 1.2;
|
this.vy = (Math.random() - 0.5) * 1.2;
|
||||||
|
|
||||||
// Ukuran random (2-5px)
|
|
||||||
this.size = Math.random() * 3 + 2;
|
this.size = Math.random() * 3 + 2;
|
||||||
|
|
||||||
// Array warna neon untuk partikel
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#00d9ff', // Cyan
|
'#00d9ff',
|
||||||
'#ff00ff', // Magenta
|
'#ff00ff',
|
||||||
'#00ffff', // Cyan bright
|
'#00ffff',
|
||||||
'#ff0080', // Pink
|
'#ff0080',
|
||||||
'#9d00ff', // Purple
|
'#9d00ff',
|
||||||
'#00ff88' // Green
|
'#00ff88'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Pilih warna random dari array
|
|
||||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||||
this.element.style.background = color;
|
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.width = `${this.size}px`;
|
||||||
this.element.style.height = `${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() {
|
updatePosition() {
|
||||||
this.element.style.left = `${this.x}px`;
|
this.element.style.left = `${this.x}px`;
|
||||||
this.element.style.top = `${this.y}px`;
|
this.element.style.top = `${this.y}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
MOVE - Update posisi berdasarkan velocity
|
|
||||||
==========================================
|
|
||||||
Fitur: Wrap-around screen (partikel keluar = muncul sisi lain)
|
|
||||||
========================================== */
|
|
||||||
move() {
|
move() {
|
||||||
// Update posisi berdasarkan kecepatan
|
|
||||||
this.x += this.vx;
|
this.x += this.vx;
|
||||||
this.y += this.vy;
|
this.y += this.vy;
|
||||||
|
|
||||||
// WRAP-AROUND LOGIC
|
|
||||||
// Kalau keluar dari kiri → muncul dari kanan
|
|
||||||
if (this.x < -10) this.x = window.innerWidth + 10;
|
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;
|
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;
|
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;
|
if (this.y > window.innerHeight + 10) this.y = -10;
|
||||||
|
|
||||||
this.updatePosition(); // Sync ke DOM
|
this.updatePosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
const cols = 15;
|
||||||
EVEN DISTRIBUTION - Distribusi Partikel Merata
|
const rows = 10;
|
||||||
==========================================
|
let particleIndex = 0;
|
||||||
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
|
|
||||||
|
|
||||||
// Loop baris
|
|
||||||
for (let i = 0; i < rows; i++) {
|
for (let i = 0; i < rows; i++) {
|
||||||
// Loop kolom
|
|
||||||
for (let j = 0; j < cols; j++) {
|
for (let j = 0; j < cols; j++) {
|
||||||
// Guard: stop kalau sudah 150 partikel
|
|
||||||
if (particleIndex >= particleCount) break;
|
if (particleIndex >= particleCount) break;
|
||||||
|
|
||||||
const particle = new Particle();
|
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.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100;
|
||||||
particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100;
|
particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100;
|
||||||
particle.updatePosition();
|
particle.updatePosition();
|
||||||
|
|
||||||
particles.push(particle); // Simpan ke array
|
particles.push(particle);
|
||||||
particleIndex++;
|
particleIndex++;
|
||||||
}
|
}
|
||||||
if (particleIndex >= particleCount) break;
|
if (particleIndex >= particleCount) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
|
||||||
ANIMATE - Main Animation Loop
|
|
||||||
==========================================
|
|
||||||
Menggunakan requestAnimationFrame untuk performa optimal
|
|
||||||
(60 FPS, smooth, GPU-accelerated)
|
|
||||||
========================================== */
|
|
||||||
function animate() {
|
function animate() {
|
||||||
// Loop semua partikel dan gerakkan
|
|
||||||
particles.forEach(p => p.move());
|
particles.forEach(p => p.move());
|
||||||
|
|
||||||
// Request next frame (recursive call)
|
|
||||||
// Browser otomatis sync dengan refresh rate monitor
|
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start animation loop
|
|
||||||
animate();
|
animate();
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// Particle System
|
|
||||||
const particlesContainer = document.getElementById('particles');
|
const particlesContainer = document.getElementById('particles');
|
||||||
const particleCount = 150;
|
const particleCount = 150;
|
||||||
const particles = [];
|
const particles = [];
|
||||||
@ -50,7 +49,6 @@ class Particle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create particles with even distribution
|
|
||||||
const cols = 15;
|
const cols = 15;
|
||||||
const rows = 10;
|
const rows = 10;
|
||||||
let particleIndex = 0;
|
let particleIndex = 0;
|
||||||
@ -61,7 +59,6 @@ for (let i = 0; i < rows; i++) {
|
|||||||
|
|
||||||
const particle = new Particle();
|
const particle = new Particle();
|
||||||
|
|
||||||
// Even distribution + slight random offset
|
|
||||||
particle.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100;
|
particle.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100;
|
||||||
particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100;
|
particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100;
|
||||||
particle.updatePosition();
|
particle.updatePosition();
|
||||||
@ -72,7 +69,6 @@ for (let i = 0; i < rows; i++) {
|
|||||||
if (particleIndex >= particleCount) break;
|
if (particleIndex >= particleCount) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate
|
|
||||||
function animate() {
|
function animate() {
|
||||||
particles.forEach(p => p.move());
|
particles.forEach(p => p.move());
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// Mengizinkan akses dari semua domain
|
|
||||||
header('Access-Control-Allow-Origin: *');
|
header('Access-Control-Allow-Origin: *');
|
||||||
// Menentukan metode HTTP yang diizinkan (POST, GET, OPTIONS)
|
|
||||||
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||||
// Menentukan header yang diizinkan dalam request
|
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||||
// Mengatur tipe konten output menjadi format JSON
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$DB_HOST = "202.46.28.160";
|
$DB_HOST = "202.46.28.160";
|
||||||
@ -14,7 +10,6 @@ $DB_USER = "evelyn";
|
|||||||
$DB_PASS = "evelynsc25";
|
$DB_PASS = "evelynsc25";
|
||||||
$DB_NAME = "web";
|
$DB_NAME = "web";
|
||||||
|
|
||||||
// Memeriksa apakah ada error saat menghubungkan ke database
|
|
||||||
$conn = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME, $DB_PORT);
|
$conn = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME, $DB_PORT);
|
||||||
if ($conn->connect_error) {
|
if ($conn->connect_error) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|||||||
11
Homepage.css
11
Homepage.css
@ -20,7 +20,6 @@ body {
|
|||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animated Background Grid */
|
|
||||||
body::before {
|
body::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -42,7 +41,6 @@ body::before {
|
|||||||
100% { transform: translate(50px, 50px); }
|
100% { transform: translate(50px, 50px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Neon Particles */
|
|
||||||
#particles {
|
#particles {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -75,32 +73,25 @@ body::before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Atur lebar scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Background jalanan scrollbar (Track) - Gelap agar neon menyala */
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #0b0b15; /* Hitam keunguan sangat gelap */
|
background: #0b0b15;
|
||||||
border-left: 1px solid #222;
|
border-left: 1px solid #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Batang Scroll (Thumb) - Efek Neon */
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
/* Gradasi dari Cyan ke Ungu */
|
|
||||||
background: linear-gradient(180deg, #00f2ff, #bc13fe);
|
background: linear-gradient(180deg, #00f2ff, #bc13fe);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
/* Memberikan jarak sedikit agar terlihat melayang */
|
|
||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Efek saat di-hover (disorot mouse) - Lebih terang */
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: linear-gradient(180deg, #5dfaff, #d65eff);
|
background: linear-gradient(180deg, #5dfaff, #d65eff);
|
||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
/* Sedikit shadow untuk efek glow */
|
|
||||||
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5);
|
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
@ -13,20 +13,14 @@
|
|||||||
<link rel="stylesheet" href="Homepage_Responsive.css">
|
<link rel="stylesheet" href="Homepage_Responsive.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Neon Particles Background -->
|
|
||||||
<div id="particles"></div>
|
<div id="particles"></div>
|
||||||
|
|
||||||
<!-- Main Content Wrapper -->
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- Header -->
|
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">2048</div>
|
<div class="logo">2048</div>
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="Login.html" class="btn btn-login">Login</a>
|
<a href="Login.html" class="btn btn-login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Hero Section -->
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-title">2048</div>
|
<div class="hero-title">2048</div>
|
||||||
<p class="hero-subtitle">Join the tiles, reach 2048</p>
|
<p class="hero-subtitle">Join the tiles, reach 2048</p>
|
||||||
@ -36,8 +30,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer with Credits -->
|
|
||||||
<footer>
|
<footer>
|
||||||
<hr class="footer-hr">
|
<hr class="footer-hr">
|
||||||
<div class="credits">
|
<div class="credits">
|
||||||
@ -46,8 +38,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Logout Confirmation Modal -->
|
|
||||||
<div class="logout-overlay" id="logout-overlay" style="display: none">
|
<div class="logout-overlay" id="logout-overlay" style="display: none">
|
||||||
<div class="logout-modal">
|
<div class="logout-modal">
|
||||||
<div class="logout-icon">
|
<div class="logout-icon">
|
||||||
@ -59,15 +49,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2 class="logout-title">Logout Confirmation</h2>
|
<h2 class="logout-title">Logout Confirmation</h2>
|
||||||
<p class="logout-message">Are you sure you want to logout?</p>
|
<p class="logout-message">Are you sure you want to logout?</p>
|
||||||
|
|
||||||
<div class="logout-buttons">
|
<div class="logout-buttons">
|
||||||
<button class="btn-logout-cancel" id="btn-logout-cancel">Cancel</button>
|
<button class="btn-logout-cancel" id="btn-logout-cancel">Cancel</button>
|
||||||
<button class="btn-logout-confirm" id="btn-logout-confirm">Logout</button>
|
<button class="btn-logout-confirm" id="btn-logout-confirm">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logout Success Modal -->
|
|
||||||
<div class="logout-success-overlay" id="logout-success-overlay" style="display: none">
|
<div class="logout-success-overlay" id="logout-success-overlay" style="display: none">
|
||||||
<div class="logout-success-modal">
|
<div class="logout-success-modal">
|
||||||
<div class="success-icon">
|
<div class="success-icon">
|
||||||
@ -79,9 +66,7 @@
|
|||||||
<p class="success-message">You have been logged out successfully</p>
|
<p class="success-message">You have been logged out successfully</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Auto-update copyright year
|
|
||||||
document.getElementById('currentYear').textContent = new Date().getFullYear();
|
document.getElementById('currentYear').textContent = new Date().getFullYear();
|
||||||
</script>
|
</script>
|
||||||
<script src="Animation_Homepage.js"></script>
|
<script src="Animation_Homepage.js"></script>
|
||||||
|
|||||||
39
Homepage.js
39
Homepage.js
@ -1,17 +1,15 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ==================== DOM ELEMENTS ====================
|
|
||||||
const elements = {
|
const elements = {
|
||||||
logo: null,
|
logo: null,
|
||||||
loginBtn: null, // Ini akan di-update oleh logout.js
|
loginBtn: null,
|
||||||
playBtn: null,
|
playBtn: null,
|
||||||
leaderboardBtn: null,
|
leaderboardBtn: null,
|
||||||
heroTitle: null,
|
heroTitle: null,
|
||||||
heroSubtitle: null
|
heroSubtitle: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== INITIALIZE ====================
|
|
||||||
function init() {
|
function init() {
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initAll);
|
document.addEventListener('DOMContentLoaded', initAll);
|
||||||
@ -22,19 +20,15 @@
|
|||||||
|
|
||||||
function initAll() {
|
function initAll() {
|
||||||
try {
|
try {
|
||||||
// Cache DOM elements
|
|
||||||
cacheElements();
|
cacheElements();
|
||||||
|
|
||||||
// Validate elements
|
|
||||||
if (!validateElements()) {
|
if (!validateElements()) {
|
||||||
console.error('Some required elements are missing');
|
console.error('Some required elements are missing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup event listeners
|
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
|
||||||
// Initialize smooth scroll
|
|
||||||
initSmoothScroll();
|
initSmoothScroll();
|
||||||
|
|
||||||
console.log('✅ Homepage initialized successfully');
|
console.log('✅ Homepage initialized successfully');
|
||||||
@ -43,11 +37,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== CACHE ELEMENTS ====================
|
|
||||||
function cacheElements() {
|
function cacheElements() {
|
||||||
elements.logo = document.querySelector('.logo');
|
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.loginBtn = document.querySelector('.btn-login') || document.querySelector('.btn-logout');
|
||||||
elements.playBtn = document.querySelector('a[href="2048.html"]');
|
elements.playBtn = document.querySelector('a[href="2048.html"]');
|
||||||
elements.leaderboardBtn = document.querySelector('a[href="leaderboard.html"]');
|
elements.leaderboardBtn = document.querySelector('a[href="leaderboard.html"]');
|
||||||
@ -55,7 +46,6 @@
|
|||||||
elements.heroSubtitle = document.querySelector('.hero-subtitle');
|
elements.heroSubtitle = document.querySelector('.hero-subtitle');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== VALIDATE ELEMENTS ====================
|
|
||||||
function validateElements() {
|
function validateElements() {
|
||||||
const requiredElements = ['logo', 'playBtn'];
|
const requiredElements = ['logo', 'playBtn'];
|
||||||
const missingElements = requiredElements.filter(key => !elements[key]);
|
const missingElements = requiredElements.filter(key => !elements[key]);
|
||||||
@ -68,59 +58,46 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== EVENT LISTENERS ====================
|
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
// Logo click - reload page
|
|
||||||
if (elements.logo) {
|
if (elements.logo) {
|
||||||
elements.logo.addEventListener('click', handleLogoClick);
|
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) {
|
if (elements.playBtn) {
|
||||||
elements.playBtn.addEventListener('click', handlePlayClick);
|
elements.playBtn.addEventListener('click', handlePlayClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaderboard button
|
|
||||||
if (elements.leaderboardBtn) {
|
if (elements.leaderboardBtn) {
|
||||||
elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick);
|
elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
|
||||||
// Window events
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== EVENT HANDLERS ====================
|
|
||||||
function handleLogoClick(e) {
|
function handleLogoClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = 'Homepage.html';
|
window.location.href = 'Homepage.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePlayClick(e) {
|
function handlePlayClick(e) {
|
||||||
// Allow default behavior (navigate to 2048.html)
|
|
||||||
console.log('Starting game...');
|
console.log('Starting game...');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLeaderboardClick(e) {
|
function handleLeaderboardClick(e) {
|
||||||
// Allow default behavior (navigate to leaderboard.html)
|
|
||||||
console.log('Opening leaderboard...');
|
console.log('Opening leaderboard...');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyPress(e) {
|
function handleKeyPress(e) {
|
||||||
// Press 'P' to play
|
|
||||||
if (e.key === 'p' || e.key === 'P') {
|
if (e.key === 'p' || e.key === 'P') {
|
||||||
if (elements.playBtn) {
|
if (elements.playBtn) {
|
||||||
elements.playBtn.click();
|
elements.playBtn.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Press 'L' to login (hanya jika belum login)
|
|
||||||
if (e.key === 'l' || e.key === 'L') {
|
if (e.key === 'l' || e.key === 'L') {
|
||||||
const loginBtn = document.querySelector('.btn-login');
|
const loginBtn = document.querySelector('.btn-login');
|
||||||
if (loginBtn) {
|
if (loginBtn) {
|
||||||
@ -128,7 +105,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Press 'B' for leaderboard
|
|
||||||
if (e.key === 'b' || e.key === 'B') {
|
if (e.key === 'b' || e.key === 'B') {
|
||||||
if (elements.leaderboardBtn) {
|
if (elements.leaderboardBtn) {
|
||||||
elements.leaderboardBtn.click();
|
elements.leaderboardBtn.click();
|
||||||
@ -137,7 +113,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
// Handle responsive behavior if needed
|
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
|
|
||||||
if (width < 768) {
|
if (width < 768) {
|
||||||
@ -150,11 +125,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBeforeUnload(e) {
|
function handleBeforeUnload(e) {
|
||||||
// Cleanup before page unload
|
|
||||||
console.log('Page unloading...');
|
console.log('Page unloading...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== SMOOTH SCROLL ====================
|
|
||||||
function initSmoothScroll() {
|
function initSmoothScroll() {
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
anchor.addEventListener('click', function(e) {
|
anchor.addEventListener('click', function(e) {
|
||||||
@ -174,7 +147,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== UTILITY FUNCTIONS ====================
|
|
||||||
function checkBrowserSupport() {
|
function checkBrowserSupport() {
|
||||||
const features = {
|
const features = {
|
||||||
localStorage: typeof(Storage) !== 'undefined',
|
localStorage: typeof(Storage) !== 'undefined',
|
||||||
@ -190,16 +162,14 @@
|
|||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== PUBLIC API ====================
|
|
||||||
window.Homepage = {
|
window.Homepage = {
|
||||||
init: initAll,
|
init: initAll,
|
||||||
isMobile: isMobile,
|
isMobile: isMobile,
|
||||||
checkSupport: checkBrowserSupport
|
checkSupport: checkBrowserSupport
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== CLEANUP ====================
|
|
||||||
window.cleanupHomepage = function() {
|
window.cleanupHomepage = function() {
|
||||||
// Remove event listeners
|
|
||||||
if (elements.logo) {
|
if (elements.logo) {
|
||||||
elements.logo.removeEventListener('click', handleLogoClick);
|
elements.logo.removeEventListener('click', handleLogoClick);
|
||||||
}
|
}
|
||||||
@ -219,7 +189,6 @@
|
|||||||
console.log('Homepage cleaned up');
|
console.log('Homepage cleaned up');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start initialization
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
@ -1,4 +1,3 @@
|
|||||||
/* General Button Styles */
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 14px 35px;
|
padding: 14px 35px;
|
||||||
border: none;
|
border: none;
|
||||||
@ -35,7 +34,6 @@
|
|||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Primary & Login Buttons */
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #00eaff, #ff00ff);
|
background: linear-gradient(135deg, #00eaff, #ff00ff);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -73,7 +71,6 @@
|
|||||||
border-color: rgba(0, 255, 255, 0.8);
|
border-color: rgba(0, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CTA Buttons (Play & Leaderboard) */
|
|
||||||
.btn-cta {
|
.btn-cta {
|
||||||
padding: 18px 45px;
|
padding: 18px 45px;
|
||||||
font-size: 19px;
|
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);
|
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 {
|
.btn-logout {
|
||||||
background: linear-gradient(135deg, #ff0066, #cc0033) !important;
|
background: linear-gradient(135deg, #ff0066, #cc0033) !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* Main Content Wrapper - Takes all space except footer */
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -7,7 +6,6 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer Section - Always at bottom */
|
|
||||||
footer {
|
footer {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@ -17,7 +15,6 @@ footer {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HR Line Above Credits */
|
|
||||||
.footer-hr {
|
.footer-hr {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
@ -27,7 +24,6 @@ footer {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Credits Container */
|
|
||||||
.credits {
|
.credits {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -36,7 +32,6 @@ footer {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Credit Text */
|
|
||||||
.credits-text {
|
.credits-text {
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@ -51,7 +46,6 @@ footer {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive - HP Only */
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
min-height: calc(100vh - 80px);
|
min-height: calc(100vh - 80px);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* Header Container */
|
|
||||||
header {
|
header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@ -12,7 +11,6 @@ header {
|
|||||||
box-shadow: 0 4px 30px rgba(0, 234, 255, 0.2);
|
box-shadow: 0 4px 30px rgba(0, 234, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo Styles */
|
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
|||||||
@ -91,7 +91,6 @@
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Floating Decoration Elements */
|
|
||||||
.hero::before {
|
.hero::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* Logout Confirmation Modal */
|
|
||||||
.logout-overlay {
|
.logout-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@ -12,7 +11,9 @@
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-overlay.active { display: flex; opacity: 1; }
|
.logout-overlay.active {
|
||||||
|
display: flex; opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.logout-modal {
|
.logout-modal {
|
||||||
background: linear-gradient(145deg, rgba(30, 0, 50, 0.98), rgba(45, 0, 70, 0.98));
|
background: linear-gradient(145deg, rgba(30, 0, 50, 0.98), rgba(45, 0, 70, 0.98));
|
||||||
@ -90,7 +91,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logout Success Modal */
|
|
||||||
.logout-success-overlay {
|
.logout-success-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@ -4,31 +4,26 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0; /* Gap tidak wajib jika sudah space-between */
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
/* Sesuaikan ukuran font agar muat dalam satu baris */
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Masukkan ini di dalam @media (max-width: 480px) */
|
|
||||||
|
|
||||||
.btn-login,
|
.btn-login,
|
||||||
.btn-logout {
|
.btn-logout {
|
||||||
padding: 6px 14px !important; /* Ukuran tombol jauh lebih kecil */
|
padding: 6px 14px !important;
|
||||||
font-size: 11px !important; /* Ukuran teks lebih kecil */
|
font-size: 11px !important;
|
||||||
letter-spacing: 1px; /* Jarak antar huruf dirapatkan */
|
letter-spacing: 1px;
|
||||||
border-radius: 8px; /* Lengkungan sudut disesuaikan */
|
border-radius: 8px;
|
||||||
min-width: auto; /* Mencegah tombol jadi terlalu lebar */
|
min-width: auto;
|
||||||
height: auto; /* Tinggi mengikuti isi teks */
|
height: auto;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Hero Section - CENTERED & BALANCED */
|
|
||||||
.hero {
|
.hero {
|
||||||
padding: 50px 15px;
|
padding: 50px 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -59,7 +54,6 @@
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logout Modal Responsive */
|
|
||||||
.logout-modal,
|
.logout-modal,
|
||||||
.logout-success-modal {
|
.logout-success-modal {
|
||||||
padding: 40px 30px;
|
padding: 40px 30px;
|
||||||
@ -85,12 +79,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- GANTI BAGIAN SCROLLBAR DENGAN INI --- */
|
|
||||||
|
|
||||||
/* Tunjuk langsung HTML & Body biar browser gak bingung */
|
|
||||||
html::-webkit-scrollbar,
|
html::-webkit-scrollbar,
|
||||||
body::-webkit-scrollbar {
|
body::-webkit-scrollbar {
|
||||||
width: 6px !important; /* Pakai !important biar maksa */
|
width: 6px !important;
|
||||||
background: #0b0b15;
|
background: #0b0b15;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,9 +92,9 @@
|
|||||||
|
|
||||||
html::-webkit-scrollbar-thumb,
|
html::-webkit-scrollbar-thumb,
|
||||||
body::-webkit-scrollbar-thumb {
|
body::-webkit-scrollbar-thumb {
|
||||||
background-color: #bc13fe !important; /* Warna Magenta */
|
background-color: #bc13fe !important;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 0 8px #bc13fe !important; /* Glow Neon */
|
box-shadow: 0 0 8px #bc13fe !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
/* Main Container */
|
|
||||||
.container {
|
.container {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
@ -38,7 +37,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Title */
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
@ -63,7 +61,6 @@ h1::before {
|
|||||||
filter: drop-shadow(0 0 10px #ffd700);
|
filter: drop-shadow(0 0 10px #ffd700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stats Layout */
|
|
||||||
.stats-container {
|
.stats-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
@ -71,7 +68,6 @@ h1::before {
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Leaderboard List Structure */
|
|
||||||
.leaderboard-list {
|
.leaderboard-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -106,7 +102,6 @@ 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 Wrapper */
|
|
||||||
.leaderboard-item {
|
.leaderboard-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -125,9 +120,6 @@ 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 LOGIC & THEMES === */
|
|
||||||
|
|
||||||
/* RANK 1: CYAN NEON */
|
|
||||||
.leaderboard-item.rank-1,
|
.leaderboard-item.rank-1,
|
||||||
.your-rank-container.rank-1 .your-rank-item {
|
.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%);
|
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;
|
filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RANK 2: MAGENTA NEON */
|
|
||||||
.leaderboard-item.rank-2,
|
.leaderboard-item.rank-2,
|
||||||
.your-rank-container.rank-2 .your-rank-item {
|
.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%);
|
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;
|
color: #ff00ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RANK 3: VIOLET NEON */
|
|
||||||
.leaderboard-item.rank-3,
|
.leaderboard-item.rank-3,
|
||||||
.your-rank-container.rank-3 .your-rank-item {
|
.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%);
|
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;
|
color: #a855f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RANK OTHER */
|
|
||||||
.leaderboard-item.rank-other {
|
.leaderboard-item.rank-other {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@ -219,7 +208,6 @@ h1::before {
|
|||||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Your Rank Section Layout */
|
|
||||||
.your-rank-container {
|
.your-rank-container {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@ -234,7 +222,7 @@ h1::before {
|
|||||||
.your-rank-container::before {
|
.your-rank-container::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1px; /* Pas di garis border */
|
top: -1px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@ -253,4 +241,4 @@ h1::before {
|
|||||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.15), rgba(0, 0, 0, 0.5));
|
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);
|
||||||
}
|
}
|
||||||
@ -9,25 +9,18 @@
|
|||||||
<link rel="stylesheet" href="Leaderboard.css"/>
|
<link rel="stylesheet" href="Leaderboard.css"/>
|
||||||
<link rel="stylesheet" href="Leaderboard_Responsive.css"/>
|
<link rel="stylesheet" href="Leaderboard_Responsive.css"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="particles"></div>
|
<div id="particles"></div>
|
||||||
|
|
||||||
<button class="btn-back" onclick="location.href='Homepage.html'">
|
<button class="btn-back" onclick="location.href='Homepage.html'">
|
||||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Leaderboard</h1>
|
<h1>Leaderboard</h1>
|
||||||
|
|
||||||
<ul class="leaderboard-list" id="leaderboardList"></ul>
|
<ul class="leaderboard-list" id="leaderboardList"></ul>
|
||||||
|
|
||||||
<div id="userRankContainer"></div>
|
<div id="userRankContainer"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="Leaderboard.js"></script>
|
<script src="Leaderboard.js"></script>
|
||||||
<script src="Animation_Leaderboard.js"></script>
|
<script src="Animation_Leaderboard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -61,8 +61,6 @@ function renderUserRank(user) {
|
|||||||
container.innerHTML = html;
|
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,17 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json'); // Response berupa JSON
|
header('Content-Type: application/json');
|
||||||
require 'Connection.php'; // Koneksi database
|
require 'Connection.php';
|
||||||
session_start(); // Ambil session user login
|
session_start();
|
||||||
|
|
||||||
// Response default
|
|
||||||
$response = [
|
$response = [
|
||||||
"status" => "error",
|
"status" => "error",
|
||||||
"leaderboard" => [],
|
"leaderboard" => [],
|
||||||
"user_rank" => null
|
"user_rank" => null
|
||||||
];
|
];
|
||||||
|
|
||||||
// Ambil Top 10 Leaderboard Global
|
|
||||||
// Urut score terbesar
|
|
||||||
$query = "
|
$query = "
|
||||||
SELECT username, score
|
SELECT username, score
|
||||||
FROM leaderboard
|
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'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
$my_id = $_SESSION['user_id'];
|
$my_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
// Ambil username & score user
|
|
||||||
$scoreQuery = $conn->prepare("
|
$scoreQuery = $conn->prepare("
|
||||||
SELECT username, score
|
SELECT username, score
|
||||||
FROM leaderboard
|
FROM leaderboard
|
||||||
@ -44,7 +39,6 @@ if (isset($_SESSION['user_id'])) {
|
|||||||
$myScore = $scoreRow['score'];
|
$myScore = $scoreRow['score'];
|
||||||
$myUsername = $scoreRow['username'];
|
$myUsername = $scoreRow['username'];
|
||||||
|
|
||||||
// Hitung jumlah user yang berada di atas
|
|
||||||
$rankQuery = $conn->prepare("
|
$rankQuery = $conn->prepare("
|
||||||
SELECT COUNT(*) AS rank_above
|
SELECT COUNT(*) AS rank_above
|
||||||
FROM leaderboard
|
FROM leaderboard
|
||||||
@ -56,7 +50,6 @@ if (isset($_SESSION['user_id'])) {
|
|||||||
$rankResult = $rankQuery->get_result();
|
$rankResult = $rankQuery->get_result();
|
||||||
$rankRow = $rankResult->fetch_assoc();
|
$rankRow = $rankResult->fetch_assoc();
|
||||||
|
|
||||||
// Rank = jumlah di atas + 1
|
|
||||||
$response['user_rank'] = [
|
$response['user_rank'] = [
|
||||||
"username" => $myUsername,
|
"username" => $myUsername,
|
||||||
"score" => $myScore,
|
"score" => $myScore,
|
||||||
@ -65,12 +58,9 @@ if (isset($_SESSION['user_id'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status sukses
|
|
||||||
$response['status'] = "success";
|
$response['status'] = "success";
|
||||||
|
|
||||||
// Kirim JSON ke client
|
|
||||||
echo json_encode($response);
|
echo json_encode($response);
|
||||||
|
|
||||||
// Tutup koneksi database
|
|
||||||
$conn->close();
|
$conn->close();
|
||||||
?>
|
?>
|
||||||
@ -1,5 +1,3 @@
|
|||||||
/* Leaderboard_Base.css */
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -20,7 +18,6 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Neon Particles */
|
|
||||||
#particles {
|
#particles {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -40,30 +37,23 @@ body {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Leaderboard_Base.css - Bagian Paling Bawah */
|
|
||||||
|
|
||||||
/* === KUSTOMISASI SCROLLBAR BROWSER UTAMA === */
|
|
||||||
/* Lebar Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
background: #0b0015; /* Latar belakang track gelap */
|
background: #0b0015;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track (Jalur) */
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Thumb (Batang Scroll) */
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: linear-gradient(180deg, #3b0066, #ff00ff); /* Gradasi Ungu ke Pink */
|
background: linear-gradient(180deg, #3b0066, #ff00ff);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 3px solid #0b0015; /* Memberi jarak agar terlihat melayang */
|
border: 3px solid #0b0015;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Efek Hover di Scrollbar */
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: linear-gradient(180deg, #5e00a3, #ff33ff);
|
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);
|
||||||
}
|
}
|
||||||
@ -146,7 +146,6 @@ input::placeholder {
|
|||||||
text-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
text-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Modal Styles */
|
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -168,18 +167,15 @@ input::placeholder {
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: linear-gradient(135deg, rgba(30, 0, 50, 0.95) 0%, rgba(50, 0, 80, 0.95) 100%);
|
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: 2px solid rgba(0, 217, 255, 0.3);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 40px 30px;
|
padding: 40px 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
/* Box shadow ditambahkan efek neon (luar dan dalam) */
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5),
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5),
|
||||||
0 0 40px rgba(0, 217, 255, 0.3),
|
0 0 40px rgba(0, 217, 255, 0.3),
|
||||||
inset 0 0 30px rgba(0, 217, 255, 0.1);
|
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;
|
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 {
|
input::-webkit-calendar-picker-indicator {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styling untuk browser autocomplete */
|
|
||||||
input:-webkit-autofill,
|
input:-webkit-autofill,
|
||||||
input:-webkit-autofill:hover,
|
input:-webkit-autofill:hover,
|
||||||
input:-webkit-autofill:focus {
|
input:-webkit-autofill:focus {
|
||||||
@ -294,7 +288,6 @@ input:-webkit-autofill:focus {
|
|||||||
border-color: #00d9ff;
|
border-color: #00d9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styling untuk datalist dropdown - terbatas karena browser restrictions */
|
|
||||||
datalist {
|
datalist {
|
||||||
background-color: rgba(30, 0, 50, 0.95);
|
background-color: rgba(30, 0, 50, 0.95);
|
||||||
}
|
}
|
||||||
@ -305,12 +298,10 @@ option {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Untuk Firefox */
|
|
||||||
input::-moz-list-thumbnail {
|
input::-moz-list-thumbnail {
|
||||||
background-color: rgba(30, 0, 50, 0.95);
|
background-color: rgba(30, 0, 50, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom styling untuk dropdown suggestion */
|
|
||||||
input[list]::-webkit-list-button {
|
input[list]::-webkit-list-button {
|
||||||
color: #00d9ff;
|
color: #00d9ff;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,15 +8,12 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="particles"></div>
|
<div id="particles"></div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
|
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="username" placeholder="Username" required />
|
<input type="text" id="username" placeholder="Username" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -25,16 +22,13 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn">Login</button>
|
<button type="submit" class="btn">Login</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="register-link">
|
<div class="register-link">
|
||||||
Don't Have An Account?
|
Don't Have An Account?
|
||||||
<a href="Register.html" id="registerLink">Register</a>
|
<a href="Register.html" id="registerLink">Register</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="customModal" class="modal">
|
<div id="customModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-icon" id="modalIcon"></div>
|
<div class="modal-icon" id="modalIcon"></div>
|
||||||
@ -43,7 +37,6 @@
|
|||||||
<button id="modalOkBtn" class="modal-btn">OK</button>
|
<button id="modalOkBtn" class="modal-btn">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="Login.js"></script>
|
<script type="module" src="Login.js"></script>
|
||||||
<script src="Animation_Login.js"></script>
|
<script src="Animation_Login.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
2
Login.js
2
Login.js
@ -1,7 +1,6 @@
|
|||||||
import { showModal, setupModalOk, setupOutsideClose } from "./Modal_Login.js";
|
import { showModal, setupModalOk, setupOutsideClose } from "./Modal_Login.js";
|
||||||
import { loginRequest } from "./Login_Request.js";
|
import { loginRequest } from "./Login_Request.js";
|
||||||
|
|
||||||
// ✅ Setup modal saat halaman load
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
setupModalOk();
|
setupModalOk();
|
||||||
setupOutsideClose();
|
setupOutsideClose();
|
||||||
@ -27,7 +26,6 @@ document.getElementById('loginForm').addEventListener('submit', async function(e
|
|||||||
console.log('Response dari server:', data);
|
console.log('Response dari server:', data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// ✅ PERBAIKAN: Gunakan sessionStorage (sama dengan logout.js)
|
|
||||||
sessionStorage.setItem('authToken', data.token);
|
sessionStorage.setItem('authToken', data.token);
|
||||||
sessionStorage.setItem('loggedInUser', data.username);
|
sessionStorage.setItem('loggedInUser', data.username);
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,6 @@ session_destroy();
|
|||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"status" => "success",
|
"status" => "success",
|
||||||
"message" => "Logout berhasil"
|
"message" => "Logout successful."
|
||||||
]);
|
]);
|
||||||
?>
|
?>
|
||||||
@ -13,10 +13,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="particles"></div>
|
<div id="particles"></div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>REGISTER</h1>
|
<h1>REGISTER</h1>
|
||||||
|
|
||||||
<form id="registerForm">
|
<form id="registerForm">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
@ -27,7 +25,6 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -37,7 +34,6 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -47,17 +43,13 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn">Register</button>
|
<button type="submit" class="btn">Register</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="register-link">
|
<div class="register-link">
|
||||||
Already Have An Account?
|
Already Have An Account?
|
||||||
<a href="Login.html" id="loginLink">Login</a>
|
<a href="Login.html" id="loginLink">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="customModal" class="modal">
|
<div id="customModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-icon" id="modalIcon"></div>
|
<div class="modal-icon" id="modalIcon"></div>
|
||||||
@ -66,7 +58,6 @@
|
|||||||
<button id="modalOkBtn" class="modal-btn">OK</button>
|
<button id="modalOkBtn" class="modal-btn">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="Score_Request.js"></script>
|
<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>
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
/* register-autofill.css */
|
|
||||||
/* Autocomplete/Datalist Styling */
|
|
||||||
input::-webkit-calendar-picker-indicator {
|
input::-webkit-calendar-picker-indicator {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Browser autocomplete styling */
|
|
||||||
input:-webkit-autofill,
|
input:-webkit-autofill,
|
||||||
input:-webkit-autofill:hover,
|
input:-webkit-autofill:hover,
|
||||||
input:-webkit-autofill:focus {
|
input:-webkit-autofill:focus {
|
||||||
@ -18,7 +15,6 @@ input:-webkit-autofill:focus {
|
|||||||
border-color: #00d9ff;
|
border-color: #00d9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Datalist dropdown styling - limited due to browser restrictions */
|
|
||||||
datalist {
|
datalist {
|
||||||
background-color: rgba(30, 0, 50, 0.95);
|
background-color: rgba(30, 0, 50, 0.95);
|
||||||
}
|
}
|
||||||
@ -29,12 +25,10 @@ option {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For Firefox */
|
|
||||||
input::-moz-list-thumbnail {
|
input::-moz-list-thumbnail {
|
||||||
background-color: rgba(30, 0, 50, 0.95);
|
background-color: rgba(30, 0, 50, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom styling for dropdown suggestion */
|
|
||||||
input[list]::-webkit-list-button {
|
input[list]::-webkit-list-button {
|
||||||
color: #00d9ff;
|
color: #00d9ff;
|
||||||
}
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
/* register-base.css */
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -16,7 +15,6 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Neon Particles */
|
|
||||||
#particles {
|
#particles {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* register-container.css */
|
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user