// PLAYER STATE let nickname = ""; let gamePin = ""; let channel = null; let heartbeatInterval = null; let currentQuestionTimeLimit = 0; let timeRemaining = 0; let playerTimer = null; let answered = false; // DOM ELEMENTS const screenJoin = document.getElementById("screen-join"); const screenLobbyWait = document.getElementById("screen-lobby-wait"); const screenAnswer = document.getElementById("screen-answer"); const screenSubmitted = document.getElementById("screen-submitted"); const screenFeedback = document.getElementById("screen-feedback"); const screenWaitNext = document.getElementById("screen-wait-next"); const screenGameOver = document.getElementById("screen-game-over"); const inputPin = document.getElementById("input-pin"); const inputNickname = document.getElementById("input-nickname"); const joinGameBtn = document.getElementById("join-game-btn"); const backHomeBtn = document.getElementById("back-home-btn"); const joinErrorMsg = document.getElementById("join-error-msg"); const playerWelcomeName = document.getElementById("player-welcome-name"); const playerStatusBar = document.getElementById("player-status-bar"); const feedbackContent = document.getElementById("feedback-content"); const feedbackIcon = document.getElementById("feedback-icon"); const feedbackTitle = document.getElementById("feedback-title"); const feedbackPoints = document.getElementById("feedback-points"); const feedbackStreak = document.getElementById("feedback-streak"); const feedbackStanding = document.getElementById("feedback-standing"); const gameOverEmoji = document.getElementById("game-over-emoji"); const gameOverTitle = document.getElementById("game-over-title"); const gameOverDesc = document.getElementById("game-over-desc"); const gameOverScore = document.getElementById("game-over-score"); const playerExitBtn = document.getElementById("player-exit-btn"); // INIT function init() { joinGameBtn.addEventListener("click", attemptJoin); backHomeBtn.addEventListener("click", () => { window.location.href = "/"; }); playerExitBtn.addEventListener("click", () => { window.location.href = "/"; }); // Enable join on enter inputPin.addEventListener("keyup", (event) => { if (event.key === "Enter") attemptJoin(); }); inputNickname.addEventListener("keyup", (event) => { if (event.key === "Enter") attemptJoin(); }); // Query parameter auto-fill const urlParams = new URLSearchParams(window.location.search); const pinParam = urlParams.get('pin'); if (pinParam) { inputPin.value = pinParam; inputNickname.focus(); } } function attemptJoin() { gamePin = inputPin.value.trim(); nickname = inputNickname.value.trim(); if (!gamePin || gamePin.length !== 6) { showError("PIN game harus 6 angka!"); return; } if (!nickname) { showError("Nickname tidak boleh kosong!"); return; } joinErrorMsg.classList.add("hidden"); joinGameBtn.innerText = "Menghubungkan..."; joinGameBtn.disabled = true; // Initialize BroadcastChannel with PIN initPlayerChannel(); } function showError(message) { joinErrorMsg.innerText = message; joinErrorMsg.classList.remove("hidden"); joinGameBtn.innerText = "Masuk Game 🎮"; joinGameBtn.disabled = false; } // HTTP POLLING SETUP let lastStatus = "lobby"; let lastQuestionIndex = -1; let pollInterval = null; function initPlayerChannel() { // Clear any existing connection first if (pollInterval) clearInterval(pollInterval); // Send join request to Laravel Controller fetch("/game-session/player/join", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pin: gamePin, nickname: nickname }) }) .then(res => res.json()) .then(data => { if (data.success) { handleJoinSuccess(); pollInterval = setInterval(playerPoll, 1200); } else { showError(data.reason || "Gagal masuk lobi."); } }) .catch(err => { showError("Gagal menghubungi server."); console.error(err); }); } function playerPoll() { fetch(`/game-session/player/poll?pin=${gamePin}&nickname=${nickname}`) .then(res => res.json()) .then(data => { if (!data.success) { showError(data.message || "Koneksi terputus."); if (pollInterval) clearInterval(pollInterval); return; } const status = data.status; const currentQuestionIndex = data.currentQuestionIndex; const timeLimit = data.timeLimit || 20; const self = data.self; // Track status transitions if (status === "lobby") { lastStatus = "lobby"; } else if (status === "question") { if (lastStatus !== "question" || lastQuestionIndex !== currentQuestionIndex) { lastStatus = "question"; lastQuestionIndex = currentQuestionIndex; handleQuestionStart(timeLimit); } } else if (status === "answers_shown") { if (lastStatus !== "answers_shown") { lastStatus = "answers_shown"; // Render feedback screen using 'self' stats! if (self) { handleQuestionEnd({ correct: self.lastCorrect, pointsEarned: self.pointsEarned || 0, streak: self.streak || 0, rank: self.rank || 1, score: self.score || 0 }); } } } else if (status === "leaderboard") { if (lastStatus !== "leaderboard") { lastStatus = "leaderboard"; // Switch to waiting screen showWaitScreen(); } } else if (status === "ended") { if (lastStatus !== "ended") { lastStatus = "ended"; // Extract podium standings from player list const sorted = [...data.players].sort((a, b) => b.score - a.score); const podium = sorted.slice(0, 3).map((p, idx) => ({ nickname: p.nickname, score: p.score, rank: idx + 1 })); handleGameEnd(podium); if (pollInterval) clearInterval(pollInterval); } } }) .catch(err => console.warn("Player poll failed:", err)); } function sendToChannel(message) { // Map WS player message to HTTP POST if (message.type === "answer") { fetch("/game-session/player/submit-answer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pin: gamePin, nickname: nickname, optionIndex: message.optionIndex, timeRemaining: message.timeRemaining }) }); } } // EVENT HANDLERS function handleJoinSuccess() { playerWelcomeName.innerText = `Halo, ${escapeHTML(nickname)}!`; playerStatusBar.innerText = `PIN: ${gamePin} • ${nickname}`; // Transition screens screenJoin.classList.add("hidden"); screenLobbyWait.classList.remove("hidden"); } function handleGameStart() { playerStatusBar.innerText = `Game Dimulai!`; screenLobbyWait.classList.add("hidden"); screenWaitNext.classList.remove("hidden"); } function handleQuestionStart(timeLimit) { answered = false; currentQuestionTimeLimit = timeLimit; timeRemaining = timeLimit; // Transition screen screenLobbyWait.classList.add("hidden"); screenWaitNext.classList.add("hidden"); screenFeedback.classList.add("hidden"); screenAnswer.classList.remove("hidden"); // Re-enable answer buttons const buttons = document.querySelectorAll(".player-btn"); buttons.forEach(btn => { btn.classList.remove("disabled"); }); playerStatusBar.innerText = `Pilih Jawaban Cepat!`; // Start internal countdown to track speed if (playerTimer) clearInterval(playerTimer); playerTimer = setInterval(() => { timeRemaining--; if (timeRemaining <= 0) { clearInterval(playerTimer); // Auto transition to wait screen if didn't answer if (!answered) { showWaitScreen(); } } }, 1000); } // ACTION: SUBMIT ANSWER (Triggered by button click in HTML) window.submitAnswer = function(optionIndex) { if (answered) return; answered = true; clearInterval(playerTimer); // Gray out other options const buttons = document.querySelectorAll(".player-btn"); buttons.forEach(btn => { btn.classList.add("disabled"); }); // Send answer payload sendToChannel({ type: "answer", nickname: nickname, optionIndex: optionIndex, timeRemaining: timeRemaining }); // Switch screen to submitted wait screen screenAnswer.classList.add("hidden"); screenSubmitted.classList.remove("hidden"); playerStatusBar.innerText = `Jawaban Terkirim`; }; function showWaitScreen() { screenAnswer.classList.add("hidden"); screenSubmitted.classList.add("hidden"); screenWaitNext.classList.remove("hidden"); playerStatusBar.innerText = `Waktu Habis`; } function handleQuestionEnd(msg) { // Clear timer in case of early finish clearInterval(playerTimer); // Transition from whatever screen screenAnswer.classList.add("hidden"); screenSubmitted.classList.add("hidden"); screenWaitNext.classList.add("hidden"); screenFeedback.classList.remove("hidden"); // Render Feedback Card if (msg.correct) { screenFeedback.className = "feedback-screen correct-bg"; feedbackIcon.innerText = "🎉"; feedbackTitle.innerText = "BENAR!"; feedbackPoints.innerText = `+${msg.pointsEarned} Poin`; feedbackPoints.style.color = "var(--color-green)"; if (msg.streak > 1) { feedbackStreak.innerText = `🔥 Streak: ${msg.streak}`; feedbackStreak.classList.remove("hidden"); } else { feedbackStreak.classList.add("hidden"); } } else { screenFeedback.className = "feedback-screen incorrect-bg"; feedbackIcon.innerText = "❌"; feedbackTitle.innerText = "SALAH"; feedbackPoints.innerText = "+0 Poin"; feedbackPoints.style.color = "var(--color-red)"; feedbackStreak.classList.add("hidden"); } feedbackStanding.innerHTML = `Posisi Ke-${msg.rank} • Skor: ${msg.score}`; playerStatusBar.innerText = `Skor: ${msg.score}`; // Automatically transition to standby view after 4 seconds setTimeout(() => { // Check if we didn't advance to a new screen or game over in between if (!screenFeedback.classList.contains("hidden")) { screenFeedback.classList.add("hidden"); screenWaitNext.classList.remove("hidden"); } }, 4000); } function handleGameEnd(podium) { // Clear any existing intervals clearInterval(playerTimer); // Hide all screens screenJoin.classList.add("hidden"); screenLobbyWait.classList.add("hidden"); screenAnswer.classList.add("hidden"); screenSubmitted.classList.add("hidden"); screenFeedback.classList.add("hidden"); screenWaitNext.classList.add("hidden"); screenGameOver.classList.remove("hidden"); playerStatusBar.innerText = `Game Selesai`; // Find player's position const rankObj = podium.find(p => p.nickname === nickname); const finalRank = rankObj ? rankObj.rank : -1; const finalScore = rankObj ? rankObj.score : 0; gameOverScore.innerText = `Skor Akhir: ${finalScore}`; if (finalRank === 1) { gameOverEmoji.innerText = "🏆"; gameOverTitle.innerText = "Juara 1!"; gameOverDesc.innerText = "Luar biasa! Anda adalah pemenang utama kuis Qriosity kali ini!"; } else if (finalRank === 2) { gameOverEmoji.innerText = "🥈"; gameOverTitle.innerText = "Juara 2!"; gameOverDesc.innerText = "Hebat sekali! Anda menduduki posisi runner-up!"; } else if (finalRank === 3) { gameOverEmoji.innerText = "🥉"; gameOverTitle.innerText = "Juara 3!"; gameOverDesc.innerText = "Kerja bagus! Anda berhasil naik ke podium juara!"; } else { gameOverEmoji.innerText = "👏"; gameOverTitle.innerText = "Terima Kasih!"; // Find rank in full list gameOverDesc.innerText = `Anda menyelesaikan kuis ini di peringkat ke-X. Tingkatkan terus belajarmu!`; // We can show score if rank is unknown if (finalRank > 3) { gameOverDesc.innerText = `Anda menyelesaikan kuis di peringkat ke-${finalRank}!`; } else { gameOverDesc.innerText = `Terima kasih telah berpartisipasi dalam kuis ini!`; } } } // HELPERS function escapeHTML(str) { return str.replace(/[&<>'"]/g, tag => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[tag] || tag) ); } // LOAD INIT document.addEventListener("DOMContentLoaded", init); window.onbeforeunload = () => { if (channel) { channel.close(); } };