From 71f7fd77303ccac9cc0f1844c281a4f71f3fd40e Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 18 Dec 2025 13:25:13 +0700 Subject: [PATCH] hard mode finally fixed including score. proceeiding into leaderboard --- assets/gameboard-easy.js | 5 +- assets/gameboard-hard.css | 209 +++++++++++++++++++++++-------------- assets/gameboard-hard.js | 147 ++++++++++++++------------ assets/gameboard-medium.js | 4 - 4 files changed, 209 insertions(+), 156 deletions(-) diff --git a/assets/gameboard-easy.js b/assets/gameboard-easy.js index 105ad31..5ae7f5b 100644 --- a/assets/gameboard-easy.js +++ b/assets/gameboard-easy.js @@ -40,9 +40,6 @@ toggleBtn.onclick = (e) => { musicMuted = !musicMuted; }; -// --- GAME LOGIC --- - -// !!! PENTING: Jika untuk MEDIUM/HARD, tambah gambar di sini !!! const images = [ "images/fruit1.png", "images/fruit2.png", "images/fruit3.png", "images/fruit4.png", "images/fruit5.png", "images/fruit6.png" @@ -51,7 +48,7 @@ const images = [ let cards = [...images, ...images]; let flipped = []; let timerStarted = false; -let time = 60; // !!! PENTING: Ganti waktu di sini untuk Medium/Hard !!! +let time = 60; let moves = 0; let score = 0; let countdown; diff --git a/assets/gameboard-hard.css b/assets/gameboard-hard.css index 740ff17..07f5cc2 100644 --- a/assets/gameboard-hard.css +++ b/assets/gameboard-hard.css @@ -1,7 +1,7 @@ html, body { margin: 0; - height: 100vh; - overflow: hidden; /* Wajib: Kunci layar biar gak bisa discroll */ + height: 100%; + overflow: hidden; font-family: "Poppins", sans-serif; } @@ -11,58 +11,81 @@ body { flex-direction: column; } -/* --- TOPBAR --- */ +/* --- OVERLAY COUNTDOWN --- */ +#countdown-overlay { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.85); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + color: white; + font-size: 150px; + font-weight: 800; + display: none; +} + +.pulse-anim { + animation: pulse 1s ease-out infinite; +} + +@keyframes pulse { + 0% { transform: scale(1); opacity: 1; } + 100% { transform: scale(2); opacity: 0; } +} + .topbar { - width: 100%; - height: 60px; /* Tinggi header kita kunci */ - box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; - padding: 0 20px; - background: rgba(255, 255, 255, 0.9); - border-bottom: 2px solid rgba(0,0,0,0.1); + padding: 8px 14px; + background: rgb(255, 255, 255); + border-bottom: 2px solid rgba(39, 35, 35, 0.6); + backdrop-filter: blur(14px); z-index: 50; - flex-shrink: 0; } -.back-btn { font-size: 26px; border: none; background: none; cursor: pointer; color: #222; } -.right-info { display: flex; gap: 10px; } +.back-btn { + font-size: 28px; + background: none; + border: none; + cursor: pointer; + color: #222; +} + +.right-info { + display: flex; + gap: 10px; +} + .pill { - display: flex; align-items: center; gap: 5px; padding: 5px 15px; - background: white; border-radius: 20px; border: 1px solid #ccc; - color: #003366; font-weight: 600; font-size: 15px; + display: flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + background: white; + border-radius: 20px; + border: 2px solid rgba(255,255,255,0.8); + font-weight: 600; + box-shadow: 0 3px 6px rgba(0,0,0,0.2), inset 0 0 6px rgba(255,255,255,0.6); } -.icon { font-size: 18px; } -/* --- GAMEBOARD FULL SCREEN --- */ .gameboard { - /* 1. Ambil SEMUA sisa tinggi layar (100% layar - 60px header) */ - height: calc(100vh - 60px); - - /* 2. Ambil SEMUA lebar layar */ - width: 100%; - max-width: none; /* Hapus batasan lebar yang bikin kecil kemarin */ - + flex: 1; display: grid; - /* Grid 4 Kolom x 5 Baris */ grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(5, 1fr); - - gap: 10px; /* Jarak antar kartu sedikit dilonggarkan */ - padding: 10px; /* Jarak dari pinggir layar */ + gap: 16px; + padding: 25px 40px; box-sizing: border-box; + place-items: center; } -/* --- KARTU --- */ .card { width: 100%; height: 100%; perspective: 1000px; cursor: pointer; - /* PENTING: min-height 0 supaya kartu mau mengecil/memipih menyesuaikan grid */ - min-height: 0; - min-width: 0; } .inner { @@ -70,7 +93,7 @@ body { height: 100%; position: relative; transform-style: preserve-3d; - transition: transform 0.4s; + transition: transform 0.55s ease; border-radius: 10px; } @@ -83,74 +106,98 @@ body { justify-content: center; align-items: center; border-radius: 10px; - border: 2px solid rgba(255,255,255,0.6); - box-shadow: 0 4px 6px rgba(0,0,0,0.2); + background: linear-gradient(135deg, #7a28ff 20%, #eb2bbf 80%); + border: 2px solid rgba(255,255,255,0.55); + box-shadow: 0 3px 6px rgba(0,0,0,0.25); } .front { - background: linear-gradient(135deg, #7a28ff 20%, #eb2bbf 80%); + font-size: calc(10px + 2vw); + font-weight: 700; color: #ff6a4d; - /* Font ? dibuat besar tapi responsif */ - font-size: 5vh; - font-weight: 800; } -.back { - background: white; - transform: rotateY(180deg); -} - -/* Agar gambar di dalam kartu tetap proporsional (tidak gepeng) */ +.back { transform: rotateY(180deg); } .back img { - width: 70%; - height: 70%; - object-fit: contain; + width: 85%; + height: 85%; + object-fit: contain; + filter: drop-shadow(0 3px 4px rgba(0,0,0,0.45)); } .card.flipped .inner { transform: rotateY(180deg); } -.card.matched .front, .card.matched .back { - border-color: #7affd6; - box-shadow: 0 0 15px #7affd6; -} -/* --- ELEMENTS LAINNYA --- */ -#countdown-overlay { - position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.85); display: none; - justify-content: center; align-items: center; z-index: 1000; - color: white; font-size: 100px; font-weight: 800; +.card.matched .front, +.card.matched .back { + border-color: #7affd6; + box-shadow: 0 0 15px #7affd6, 0 0 30px rgba(122,255,214,0.8); } -.pulse-anim { animation: pulse 1s ease-out infinite; } -@keyframes pulse { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.5); opacity: 0; } } .combo-popup { - position: absolute; padding: 8px 16px; - background: linear-gradient(135deg, #ff5f6d, #ffc371); - border-radius: 14px; color: white; font-weight: 800; - font-size: 18px; z-index: 999; border: 2px solid white; pointer-events: none; - animation: comboFade 1.2s ease forwards; + position: absolute; + padding: 12px 20px; + background: linear-gradient(135deg, rgba(255,95,109,0.92), rgba(255,0,102,0.92)); + border-radius: 14px; + color: white; + font-weight: 800; + text-align: center; + font-size: 20px; + box-shadow: 0 0 18px rgba(255, 75, 43, 0.9), 0 0 35px rgba(255, 0, 85, 0.7); + animation: comboFade 1.4s ease forwards; + pointer-events: none; + border: 2px solid rgba(255,255,255,0.35); + z-index: 99; +} + +@keyframes comboFade { + 0% { opacity: 0; transform: translate(-50%, -10px) scale(0.7); } + 20% { opacity: 1; transform: translate(-50%, -30px) scale(1); } + 100% { opacity: 0; transform: translate(-50%, -80px) scale(1.1); } } -@keyframes comboFade { to { transform: translateY(-50px); opacity: 0; } } .end-screen { - position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); - display: none; justify-content: center; align-items: center; z-index: 200; + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.45); + backdrop-filter: blur(7px); + display: none; + justify-content: center; + align-items: center; + z-index: 200; } + .end-box { - width: 90%; max-width: 400px; background: white; - border-radius: 16px; padding: 25px; text-align: center; -} -.end-btn { - width: 100%; padding: 12px; border-radius: 10px; border: none; - font-size: 16px; margin-top: 10px; cursor: pointer; font-weight: 600; + width: 80%; + max-width: 380px; + background: white; + border-radius: 22px; + padding: 20px; + text-align: center; } + +.end-title { font-size: 28px; font-weight: 700; margin-bottom: 4px; } +.score-row { display: flex; justify-content: space-between; margin: 8px 0; font-size: 18px; } +.end-btn { width: 100%; padding: 12px; border-radius: 16px; border: none; font-size: 18px; margin-top: 10px; cursor: pointer; font-weight: 600; } + .play-again { background: #b700ff; color: white; } +.leaderboard { background: gold; } .back-menu { background: #444; color: white; } .music-control { - position: fixed; bottom: 20px; right: 20px; z-index: 100; - background: white; width: 45px; height: 45px; border-radius: 50%; - display: flex; justify-content: center; align-items: center; - cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3); font-size: 20px; -} \ No newline at end of file + position: fixed; + bottom: 20px; + right: 20px; + z-index: 100; + background: white; + width: 45px; + height: 45px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + box-shadow: 0 4px 10px rgba(0,0,0,0.3); + font-size: 20px; +} + +@media (max-width: 600px) { .gameboard { grid-template-columns: repeat(4, 1fr); padding: 10px; gap: 8px; } } \ No newline at end of file diff --git a/assets/gameboard-hard.js b/assets/gameboard-hard.js index ad763a4..2b74d71 100644 --- a/assets/gameboard-hard.js +++ b/assets/gameboard-hard.js @@ -1,4 +1,3 @@ -// --- KONFIGURASI AUDIO & DOM --- const bgMusic = document.getElementById("bgMusic"); const sfxClick = document.getElementById("sfxClick"); const sfxMatch = document.getElementById("sfxMatch"); @@ -11,17 +10,21 @@ const overlay = document.getElementById("countdown-overlay"); let musicMuted = true; -// AUTO-PLAY PADA INTERAKSI PERTAMA -function initAudio() { - if(musicMuted) { - bgMusic.play().catch(() => {}); - bgMusic.volume = 0.5; - toggleBtn.textContent = "🔊"; - musicMuted = false; - document.removeEventListener('click', initAudio); +function playSFX(audio) { + if(!musicMuted) { + audio.currentTime = 0; + audio.play().catch(() => {}); } } -document.addEventListener('click', initAudio); + +document.addEventListener('click', function initAudio() { + if(musicMuted) { + musicMuted = false; + toggleBtn.textContent = "🔊"; + bgMusic.play().catch(() => {}); + document.removeEventListener('click', initAudio); + } +}); toggleBtn.onclick = (e) => { e.stopPropagation(); @@ -35,27 +38,17 @@ toggleBtn.onclick = (e) => { musicMuted = !musicMuted; }; -function playSFX(audio) { - if(!musicMuted) { - audio.currentTime = 0; - audio.play().catch(() => {}); - } -} - -// --- LOGIKA GAME HARD (10 Gambar = 20 Kartu) --- const images = [ "images/fruit1.png", "images/fruit2.png", "images/fruit3.png", "images/fruit4.png", "images/fruit5.png", "images/fruit6.png", "images/fruit7.png", "images/fruit8.png", "images/fruit9.png", "images/fruit10.png" ]; -// Total 10 gambar = 20 kartu. -// Kalau di CSS kolomnya 4, otomatis barisnya jadi 5. let cards = [...images, ...images]; let flipped = []; let timerStarted = false; -let time = 60; // Waktu Hard: 120 Detik (2 Menit) +let time = 60; let moves = 0; let score = 0; let countdown; @@ -63,12 +56,10 @@ let combo = 1; let lastMatchTime = 0; let pendingMatch = false; -// [BARU] Fungsi Visual Combo function showComboPopup(targetCard, combo, bonus) { const popup = document.createElement("div"); popup.className = "combo-popup"; popup.innerHTML = `COMBO X${combo}
+${bonus} Bonus`; - const rect = targetCard.getBoundingClientRect(); popup.style.left = rect.left + rect.width / 2 + "px"; popup.style.top = rect.top + "px"; @@ -77,22 +68,50 @@ function showComboPopup(targetCard, combo, bonus) { setTimeout(() => popup.remove(), 1500); } -// [UPDATE] Shuffle Lebih Pintar (Biar kartu sama gak sebelahan) -function shuffleDistant(cards, minDistance = 2) { + +function shuffleDistant(cards, minDistance = 4) { let valid = false; let result; + let attempts = 0; + while (!valid) { - result = [...cards].sort(() => Math.random() - 0.5); + result = [...cards]; + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [result[i], result[j]] = [result[j], result[i]]; + } + valid = true; - for (let i = 0; i < result.length - 1; i++) { - if (result[i] === result[i+1]) { // Cek tetangga - valid = false; break; + const checked = new Set(); + attempts++; + if (attempts > 2000) { + console.warn("Gagal menemukan posisi yang valid. Mengembalikan hasil acak."); + break; + } + + for (let i = 0; i < result.length; i++) { + const value = result[i]; + if (checked.has(value)) continue; + checked.add(value); + const indices = []; + for (let k = 0; k < result.length; k++) { + if (result[k] === value) indices.push(k); + } + for (let j = 0; j < indices.length - 1; j++) { + const distance = indices[j+1] - indices[j]; + if (distance < minDistance) { + valid = false; + break; } + } + + if (!valid) break; } } return result; } + function runCountdown(callback) { let count = 3; overlay.style.display = "flex"; @@ -120,45 +139,45 @@ function startTimer() { document.getElementById("timer").textContent = --time; if (time <= 0) { clearInterval(countdown); - showEndScreen(false); // Kalah + showEndScreen(false); } }, 1000); } function showEndScreen(isWin) { - clearInterval(countdown); - bgMusic.pause(); + clearInterval(countdown); + bgMusic.pause(); + if(isWin) playSFX(sfxWin); else playSFX(sfxLose); - if(isWin) playSFX(sfxWin); else playSFX(sfxLose); + const titleEl = document.getElementById("endTitleDisplay"); + const msgEl = document.getElementById("endMsgDisplay"); + if (titleEl) titleEl.textContent = isWin ? "🎉 Selamat!" : "⏰ Waktu Habis!"; + if (msgEl) msgEl.textContent = isWin ? "Anda berhasil menyelesaikan permainan!" : "Coba lagi lain kali!"; - document.getElementById("endTitle").textContent = isWin ? "🎉 Selamat!" : "⏰ Waktu Habis!"; - document.getElementById("endMsg").textContent = isWin ? "Anda berhasil menyelesaikan permainan!" : "Coba lagi lain kali!"; + let baseScore = score; + let timeBonus = isWin ? time * 5 : 0; + let moveBonus = isWin ? Math.max(0, 200 - moves * 10) : 0; + let total = baseScore + timeBonus + moveBonus; - let baseScore = score; - let timeBonus = isWin ? time * 5 : 0; - let moveBonus = isWin ? Math.max(0, 200 - moves * 10) : 0; - let total = baseScore + timeBonus + moveBonus; + document.getElementById("baseScoreEnd").textContent = baseScore; + document.getElementById("timeBonusEnd").textContent = "+" + timeBonus; + document.getElementById("moveBonusEnd").textContent = "+" + moveBonus; + document.getElementById("totalScoreEnd").textContent = total; + document.getElementById("endScreen").style.display = "flex"; - document.getElementById("baseScoreEnd").textContent = baseScore; - document.getElementById("timeBonusEnd").textContent = "+" + timeBonus; - document.getElementById("moveBonusEnd").textContent = "+" + moveBonus; - document.getElementById("totalScoreEnd").textContent = total; - document.getElementById("endScreen").style.display = "flex"; + if (isWin) { + let formData = new FormData(); + formData.append('score', total); + formData.append('difficulty', 'Hard'); - // [BARU] FETCH KE DATABASE (HARD) - if (isWin) { - let formData = new FormData(); - formData.append('score', total); - formData.append('difficulty', 'Hard'); - - fetch('score.php', { - method: 'POST', - body: formData - }) - .then(response => response.text()) - .then(result => console.log("Status Database: " + result)) - .catch(error => console.error('Error:', error)); - } + fetch('score.php', { + method: 'POST', + body: formData + }) + .then(response => response.text()) + .then(result => console.log("Status Database: " + result)) + .catch(error => console.error('Error:', error)); + } } function flipCard(card) { @@ -179,19 +198,16 @@ function flipCard(card) { if (img1 === img2) { playSFX(sfxMatch); const now = Date.now(); - - // LOGIKA COMBO & POPUP if (!pendingMatch) { pendingMatch = true; combo = 1; } else { - if (now - lastMatchTime <= 3000) { // Waktu toleransi combo 3 detik + if (now - lastMatchTime <= 3000) { combo++; let comboBonus = combo * 10; score += comboBonus; - showComboPopup(flipped[0], combo, comboBonus); // MUNCULKAN POPUP + showComboPopup(flipped[0], combo, comboBonus); } else { combo = 1; } } lastMatchTime = now; - flipped.forEach(c => c.classList.add("matched")); score += 50; document.getElementById("score").textContent = score; @@ -219,10 +235,7 @@ function startGame() { runCountdown(() => { if(!musicMuted) bgMusic.play().catch(() => {}); - - // Pakai shuffleDistant agar kartu tidak menumpuk const shuffledCards = shuffleDistant([...cards]); - shuffledCards.forEach(image => { const card = document.createElement("div"); card.className = "card"; @@ -231,7 +244,7 @@ function startGame() { board.appendChild(card); }); - time = 60; // Reset waktu jadi 120 detik + time = 60; moves = 0; score = 0; combo = 1; pendingMatch = false; flipped = []; timerStarted = false; document.getElementById("timer").textContent = time; diff --git a/assets/gameboard-medium.js b/assets/gameboard-medium.js index 8c4bc3d..a7037ea 100644 --- a/assets/gameboard-medium.js +++ b/assets/gameboard-medium.js @@ -1,4 +1,3 @@ -// --- AUDIO CONFIG --- const bgMusic = document.getElementById("bgMusic"); const sfxClick = document.getElementById("sfxClick"); const sfxMatch = document.getElementById("sfxMatch"); @@ -39,9 +38,6 @@ toggleBtn.onclick = (e) => { musicMuted = !musicMuted; }; -// --- GAME LOGIC MEDIUM --- - -// 8 GAMBAR (Total 16 Kartu) const images = [ "images/fruit1.png", "images/fruit2.png", "images/fruit3.png", "images/fruit4.png", "images/fruit5.png", "images/fruit6.png",