// 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();
}
};