From 9de14616c8946b747c990dcbc79f7c8d501f5f51 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 18 Dec 2025 02:35:13 +0700 Subject: [PATCH] perbaikan mode hard plus easy. score added --- assets/gameboard-hard.css | 245 ++++++++++++-------------------------- assets/gameboard-hard.js | 94 +++++++++++---- mainboard.php | 2 +- 3 files changed, 152 insertions(+), 189 deletions(-) diff --git a/assets/gameboard-hard.css b/assets/gameboard-hard.css index e8e03d9..52b133a 100644 --- a/assets/gameboard-hard.css +++ b/assets/gameboard-hard.css @@ -1,7 +1,7 @@ html, body { margin: 0; - height: 100vh; /* Gunakan vh agar benar-benar setinggi layar */ - overflow: hidden; /* Kunci agar tidak bisa scroll sama sekali */ + height: 100vh; + overflow: hidden; /* Layar dikunci, tidak bisa scroll */ font-family: "Poppins", sans-serif; } @@ -9,93 +9,57 @@ body { background: linear-gradient(135deg, #8929ff 30%, #ff419b 100%); display: flex; flex-direction: column; - height: 100vh; /* Memastikan body memenuhi layar */ -} - -/* 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 --- */ .topbar { + width: 100%; + height: 60px; /* Tinggi fix */ + box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; - padding: 8px 14px; - background: rgb(255, 255, 255); - border-bottom: 2px solid rgba(39, 35, 35, 0.6); - backdrop-filter: blur(14px); + padding: 0 20px; + background: rgba(255, 255, 255, 0.9); + border-bottom: 2px solid rgba(0,0,0,0.1); z-index: 50; + flex-shrink: 0; } -.back-btn { - font-size: 28px; - background: none; - border: none; - cursor: pointer; - color: #222; -} - -.right-info { - display: flex; - gap: 10px; -} - +.back-btn { font-size: 26px; border: none; background: none; cursor: pointer; color: #222; } +.right-info { display: flex; gap: 10px; } .pill { - display: flex; - align-items: center; - gap: 6px; - padding: 5px 12px; - background: rgb(255, 255, 255); - border-radius: 20px; - border: 2px solid rgba(255,255,255,0.8); - backdrop-filter: blur(8px); - color: #003366; - font-weight: 600; - box-shadow: 0 3px 6px rgba(0,0,0,0.2), inset 0 0 6px rgba(255,255,255,0.6); + 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; } - .icon { font-size: 18px; } -/* Update pada gameboard */ +/* --- GAMEBOARD (FULL SCREEN) --- */ .gameboard { - flex: 1; + /* Memenuhi sisa tinggi layar */ + height: calc(100vh - 60px); + /* Memenuhi seluruh lebar layar */ + width: 100%; + max-width: none; + display: grid; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); /* 4 Kolom */ + grid-template-rows: repeat(5, 1fr); /* 5 Baris */ + gap: 10px; padding: 10px; - box-sizing: border-box; - overflow: hidden; /* Mencegah container ini meluap */ - height: 0; /* Trik agar flex-grow (flex:1) menghitung tinggi dengan benar */ + box-sizing: border-box; } -/* Update pada card agar ukurannya proporsional */ +/* --- KARTU --- */ .card { width: 100%; height: 100%; perspective: 1000px; - /* Hapus max-height agar kartu mengikuti tinggi grid yang tersedia */ cursor: pointer; + min-height: 0; + min-width: 0; } .inner { @@ -103,10 +67,11 @@ body { height: 100%; position: relative; transform-style: preserve-3d; - transition: transform 0.55s ease; + transition: transform 0.4s; border-radius: 12px; } +/* Settingan Umum Depan & Belakang */ .front, .back { position: absolute; width: 100%; @@ -116,135 +81,81 @@ body { justify-content: center; align-items: center; border-radius: 12px; - 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), inset 0 0 6px rgba(255,255,255,0.45); + /* Border putih transparan biar cantik */ + border: 2px solid rgba(255,255,255,0.5); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); } +/* --- BAGIAN DEPAN (Tanda Tanya) --- */ .front { - font-size: calc(10px + 3vw); - font-weight: 700; + background: linear-gradient(135deg, #7a28ff 20%, #eb2bbf 80%); color: #ff6a4d; + font-size: 5vh; + font-weight: 800; } +/* --- BAGIAN BELAKANG (Gambar) --- */ .back { + /* INI PERUBAHANNYA: Background dibuat gradient juga, bukan putih lagi */ + background: linear-gradient(135deg, #7a28ff 20%, #eb2bbf 80%); transform: rotateY(180deg); } +/* Gambar di dalam kartu */ .back img { - width: 90%; - height: 90%; - object-fit: contain; - filter: drop-shadow(0 3px 4px rgba(0,0,0,0.45)); -} - -.card:not(.flipped):hover .front { - transform: scale(1.05); - transition: 0.25s; + width: 70%; + height: 70%; + object-fit: contain; + filter: drop-shadow(0 4px 4px rgba(0,0,0,0.3)); /* Efek bayangan pada buah */ } +/* Efek Balik */ .card.flipped .inner { transform: rotateY(180deg); } -.card.matched .front, -.card.matched .back { +/* Efek Cocok (Matched) - Bersinar Hijau */ +.card.matched .front, .card.matched .back { border-color: #7affd6; - box-shadow: 0 0 15px #7affd6, 0 0 30px rgba(122,255,214,0.8); + box-shadow: 0 0 15px #7affd6, inset 0 0 10px rgba(122, 255, 214, 0.5); } +/* --- 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; +} +.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: 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); -} - -@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); } + 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; } +@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.45); - backdrop-filter: blur(7px); - 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.6); backdrop-filter: blur(5px); + display: none; justify-content: center; align-items: center; z-index: 200; } - .end-box { - width: 80%; - max-width: 380px; - background: white; - border-radius: 22px; - padding: 20px; - text-align: center; - animation: fadeIn 0.4s ease; + width: 90%; max-width: 400px; background: white; + border-radius: 16px; padding: 25px; 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; + width: 100%; padding: 12px; border-radius: 10px; border: none; + font-size: 16px; margin-top: 10px; cursor: pointer; font-weight: 600; } - .play-again { background: #b700ff; color: white; } -.leaderboard { background: gold; } -.back-menu { background: #444; color: white; } +.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; -} - -@media (max-width: 700px) { - .gameboard { - grid-template-columns: repeat(2, 1fr); - padding: 12px; - gap: 10px; - } + 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 diff --git a/assets/gameboard-hard.js b/assets/gameboard-hard.js index 37ce7a1..ad763a4 100644 --- a/assets/gameboard-hard.js +++ b/assets/gameboard-hard.js @@ -42,16 +42,20 @@ function playSFX(audio) { } } -// --- LOGIKA GAME ASLI --- +// --- 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; +let time = 60; // Waktu Hard: 120 Detik (2 Menit) let moves = 0; let score = 0; let countdown; @@ -59,12 +63,34 @@ let combo = 1; let lastMatchTime = 0; let pendingMatch = false; -function shuffleArray(array) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; +// [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"; + popup.style.position = "fixed"; + document.body.appendChild(popup); + setTimeout(() => popup.remove(), 1500); +} + +// [UPDATE] Shuffle Lebih Pintar (Biar kartu sama gak sebelahan) +function shuffleDistant(cards, minDistance = 2) { + let valid = false; + let result; + while (!valid) { + result = [...cards].sort(() => Math.random() - 0.5); + valid = true; + for (let i = 0; i < result.length - 1; i++) { + if (result[i] === result[i+1]) { // Cek tetangga + valid = false; break; + } + } } - return array; + return result; } function runCountdown(callback) { @@ -109,15 +135,30 @@ function showEndScreen(isWin) { document.getElementById("endMsg").textContent = isWin ? "Anda berhasil menyelesaikan permainan!" : "Coba lagi lain kali!"; let baseScore = score; - let timeBonus = time * 5; - let moveBonus = Math.max(0, 200 - moves * 10); + 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 = "+" + (isWin ? timeBonus : 0); - document.getElementById("moveBonusEnd").textContent = "+" + (isWin ? moveBonus : 0); - document.getElementById("totalScoreEnd").textContent = isWin ? total : baseScore; + document.getElementById("timeBonusEnd").textContent = "+" + timeBonus; + document.getElementById("moveBonusEnd").textContent = "+" + moveBonus; + document.getElementById("totalScoreEnd").textContent = total; document.getElementById("endScreen").style.display = "flex"; + + // [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)); + } } function flipCard(card) { @@ -132,26 +173,32 @@ function flipCard(card) { if (flipped.length === 2) { moves++; document.getElementById("moves").textContent = moves; - let img1 = flipped[0].querySelector(".back img").src; - let img2 = flipped[1].querySelector(".back img").src; + let img1 = flipped[0].querySelector("img").src; + let img2 = flipped[1].querySelector("img").src; if (img1 === img2) { playSFX(sfxMatch); const now = Date.now(); + + // LOGIKA COMBO & POPUP if (!pendingMatch) { pendingMatch = true; combo = 1; } else { - if (now - lastMatchTime <= 3000) { + if (now - lastMatchTime <= 3000) { // Waktu toleransi combo 3 detik combo++; - score += (10 * combo); - document.getElementById("score").textContent = score; + let comboBonus = combo * 10; + score += comboBonus; + showComboPopup(flipped[0], combo, comboBonus); // MUNCULKAN POPUP } else { combo = 1; } } lastMatchTime = now; + flipped.forEach(c => c.classList.add("matched")); score += 50; document.getElementById("score").textContent = score; time += 5; + document.getElementById("timer").textContent = time; // Update tampilan waktu bonus flipped = []; + if (document.querySelectorAll(".matched").length === cards.length) { setTimeout(() => showEndScreen(true), 600); } @@ -171,8 +218,11 @@ function startGame() { board.innerHTML = ""; runCountdown(() => { - if(!musicMuted) bgMusic.play(); - const shuffledCards = shuffleArray([...cards]); + 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"; @@ -180,8 +230,10 @@ function startGame() { card.onclick = () => flipCard(card); board.appendChild(card); }); - time = 60; moves = 0; score = 0; combo = 1; - timerStarted = false; + + time = 60; // Reset waktu jadi 120 detik + moves = 0; score = 0; combo = 1; + pendingMatch = false; flipped = []; timerStarted = false; document.getElementById("timer").textContent = time; document.getElementById("moves").textContent = moves; document.getElementById("score").textContent = score; diff --git a/mainboard.php b/mainboard.php index 7a649aa..4fb4076 100644 --- a/mainboard.php +++ b/mainboard.php @@ -68,7 +68,7 @@ $roleIcon = ($roleRaw === 'admin') ? '👑' : '🎮';