diff --git a/display.html b/display.html index 1ad6acb..a8b7c30 100644 --- a/display.html +++ b/display.html @@ -35,12 +35,15 @@
+
CATEGORY - Question 1/25 + Question 1
Click "New Question" to start @@ -64,22 +67,54 @@
-
- - - - - +
+
+

📚 Source Selection

+ + +
+ +
+

🔬 Category Filter

+
+ + + + + + + + + + +
+
diff --git a/display.js b/display.js index c08571a..e066cb5 100644 --- a/display.js +++ b/display.js @@ -7,9 +7,9 @@ const questionText = document.getElementById('questionText'); const questionCategory = document.getElementById('questionCategory'); const questionNum = document.getElementById('questionNum'); const correctAnswer = document.getElementById('correctAnswer'); -const playerAnswer = document.getElementById('playerAnswer'); const answerSection = document.getElementById('answerSection'); const buzzedPlayer = document.getElementById('buzzedPlayer'); +const timerDisplay = document.getElementById('timerDisplay'); const newQuestionBtn = document.getElementById('newQuestion'); const activateBuzzerBtn = document.getElementById('activateBuzzer'); @@ -24,6 +24,64 @@ let currentQuestionNumber = 1; let currentBuzzedPlayer = null; let speechSynthesis = window.speechSynthesis; let currentUtterance = null; +let voicesLoaded = false; +let isShowingBonus = false; +let timerInterval = null; +let timeRemaining = 0; + +// Track used questions to avoid repeats +let usedQuestionIds = new Set(); +let allQuestions = []; +let questionsLoaded = false; + +// Load voices +function loadVoices() { + return new Promise((resolve) => { + let voices = speechSynthesis.getVoices(); + if (voices.length > 0) { + voicesLoaded = true; + resolve(voices); + } else { + speechSynthesis.onvoiceschanged = () => { + voices = speechSynthesis.getVoices(); + voicesLoaded = true; + resolve(voices); + }; + } + }); +} + +loadVoices().then(() => { + console.log('Voices loaded:', speechSynthesis.getVoices().length); +}); + +// Load all questions once at startup +async function loadAllQuestions() { + if (questionsLoaded) return; + + try { + console.log('Loading all questions from database...'); + const response = await fetch(CORS_PROXY + encodeURIComponent('https://scibowldb.com/api/questions'), { + method: 'GET' + }); + + if (!response.ok) { + throw new Error('Failed to load questions'); + } + + const data = await response.json(); + allQuestions = data.questions || data; + questionsLoaded = true; + console.log(`Loaded ${allQuestions.length} questions!`); + + } catch (error) { + console.error('Error loading questions:', error); + alert('Could not load question database. Will use random API instead (may repeat questions).'); + } +} + +// Start loading questions immediately +loadAllQuestions(); // Initialize game state database.ref('gameState').set({ @@ -55,63 +113,148 @@ database.ref('gameState').on('value', (snapshot) => { const playerNum = state.buzzer.playerId.replace('player', ''); currentBuzzedPlayer = state.buzzer.playerId; - // Light up the buzzer document.querySelectorAll('.light').forEach(l => l.classList.remove('active')); document.getElementById(`light${playerNum}`).classList.add('active'); buzzedPlayer.textContent = `Player ${playerNum} buzzed in!`; - // Stop reading when someone buzzes - if (currentUtterance) { + // Stop reading and timer when someone buzzes + if (currentUtterance && speechSynthesis.speaking) { speechSynthesis.cancel(); + readQuestionBtn.textContent = '🔊 Read Question'; + readQuestionBtn.disabled = false; } + stopTimer(); - // Show grading buttons markCorrectBtn.style.display = 'inline-block'; markIncorrectBtn.style.display = 'inline-block'; showAnswerBtn.style.display = 'inline-block'; } - - // Update player answer - if (state.playerAnswer) { - playerAnswer.textContent = state.playerAnswer.answer; - answerSection.style.display = 'block'; - } }); -// Fetch new question +// Timer functions +function startTimer(seconds) { + stopTimer(); + timeRemaining = seconds; + timerDisplay.textContent = `Time: ${seconds}s`; + timerDisplay.style.display = 'block'; + + timerInterval = setInterval(() => { + timeRemaining--; + timerDisplay.textContent = `Time: ${timeRemaining}s`; + + if (timeRemaining <= 0) { + stopTimer(); + timerDisplay.textContent = 'Time\'s up!'; + setTimeout(() => { + timerDisplay.style.display = 'none'; + }, 2000); + } + }, 1000); +} + +function stopTimer() { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = null; + } +} + +// Get filtered questions based on user selection +function getFilteredQuestions() { + const categories = Array.from(document.querySelectorAll('.category-checkbox:checked')) + .map(cb => cb.value); + + const source = document.getElementById('sourceSelect').value; + const roundNum = document.getElementById('roundNumber').value; + + let filtered = allQuestions.filter(q => { + // Filter by category + if (categories.length > 0 && !categories.includes(q.category)) { + return false; + } + + // Filter by source + if (source && !q.source.startsWith(source)) { + return false; + } + + // Filter by round number + if (roundNum && !q.source.includes(`round${roundNum}`)) { + return false; + } + + // Exclude already used questions + if (usedQuestionIds.has(q.id)) { + return false; + } + + return true; + }); + + return filtered; +} + +// Fetch new question with filtering newQuestionBtn.addEventListener('click', async () => { const categories = Array.from(document.querySelectorAll('.category-checkbox:checked')) .map(cb => cb.value); + if (categories.length === 0) { + alert('Please select at least one category!'); + return; + } + + // Wait for questions to load if not ready + if (!questionsLoaded) { + newQuestionBtn.textContent = 'Loading questions...'; + newQuestionBtn.disabled = true; + await loadAllQuestions(); + newQuestionBtn.textContent = 'New Question'; + newQuestionBtn.disabled = false; + } + try { - // Just fetch a random question without filtering - const response = await fetch(CORS_PROXY + encodeURIComponent(API_URL), { - method: 'GET' // Changed to GET - simpler - }); + const availableQuestions = getFilteredQuestions(); - if (!response.ok) { - throw new Error('API request failed'); + if (availableQuestions.length === 0) { + const shouldReset = confirm('No more unique questions available with these filters! Reset used questions?'); + if (shouldReset) { + usedQuestionIds.clear(); + newQuestionBtn.click(); + return; + } else { + alert('Try changing your filters or reset the game.'); + return; + } } - const data = await response.json(); + // Pick a random question from available ones + const randomIndex = Math.floor(Math.random() * availableQuestions.length); + currentQuestion = availableQuestions[randomIndex]; - // The API returns the question directly, not wrapped - currentQuestion = data; + // Mark this question as used + usedQuestionIds.add(currentQuestion.id); - console.log('Fetched question:', currentQuestion); + isShowingBonus = false; + + console.log('Selected question:', currentQuestion); + console.log('Questions remaining:', availableQuestions.length - 1); questionText.textContent = currentQuestion.tossup_question; - questionCategory.textContent = currentQuestion.category; + questionCategory.textContent = `${currentQuestion.category} - TOSSUP`; correctAnswer.textContent = currentQuestion.tossup_answer; questionNum.textContent = currentQuestionNumber; - // Update Firebase + // Show source info + if (currentQuestion.source) { + questionCategory.textContent = `${currentQuestion.category} - TOSSUP (${currentQuestion.source})`; + } + await database.ref('gameState').update({ currentQuestion: currentQuestion, buzzerActive: false, buzzer: null, - playerAnswer: null, questionNumber: currentQuestionNumber, isReading: false }); @@ -119,6 +262,7 @@ newQuestionBtn.addEventListener('click', async () => { // Reset UI answerSection.style.display = 'none'; buzzedPlayer.textContent = ''; + timerDisplay.style.display = 'none'; document.querySelectorAll('.light').forEach(l => l.classList.remove('active')); markCorrectBtn.style.display = 'none'; markIncorrectBtn.style.display = 'none'; @@ -132,50 +276,78 @@ newQuestionBtn.addEventListener('click', async () => { } catch (error) { console.error('Error fetching question:', error); - alert('Error loading question. The API might be down. Check console for details.'); + alert('Error loading question. Try refreshing the page.'); } }); // Read question aloud -readQuestionBtn.addEventListener('click', () => { +readQuestionBtn.addEventListener('click', async () => { if (!currentQuestion) { alert('Load a question first!'); return; } - // Stop any current speech - speechSynthesis.cancel(); + if (!voicesLoaded) { + await loadVoices(); + } - // Create utterance - currentUtterance = new SpeechSynthesisUtterance(currentQuestion.tossup_question); - currentUtterance.rate = 0.9; // Slightly slower for clarity - currentUtterance.pitch = 1; - currentUtterance.volume = 1; + if (speechSynthesis.speaking) { + speechSynthesis.cancel(); + await new Promise(resolve => setTimeout(resolve, 100)); + } + + const textToRead = isShowingBonus ? currentQuestion.bonus_question : currentQuestion.tossup_question; + currentUtterance = new SpeechSynthesisUtterance(textToRead); + + // Select best voice + const voices = speechSynthesis.getVoices(); + const preferredVoice = voices.find(v => v.name.includes('Google') && v.lang.startsWith('en')) || + voices.find(v => v.lang === 'en-US' && v.name.includes('Natural')) || + voices.find(v => v.lang === 'en-US') || + voices[0]; + + if (preferredVoice) { + currentUtterance.voice = preferredVoice; + } + + currentUtterance.rate = 0.9; + currentUtterance.pitch = 1.0; + currentUtterance.volume = 1.0; + + console.log('Reading with voice:', preferredVoice?.name); - // Update Firebase that we're reading database.ref('gameState/isReading').set(true); + currentUtterance.onstart = () => { + readQuestionBtn.textContent = '🔊 Reading...'; + readQuestionBtn.disabled = true; + }; + currentUtterance.onend = () => { database.ref('gameState/isReading').set(false); - console.log('Finished reading question'); + readQuestionBtn.textContent = '🔊 Read Question'; + readQuestionBtn.disabled = false; + + // Start timer after reading + const timerDuration = isShowingBonus ? 20 : 5; + startTimer(timerDuration); + }; + + currentUtterance.onerror = (event) => { + console.error('Speech error:', event); + database.ref('gameState/isReading').set(false); + readQuestionBtn.textContent = '🔊 Read Question'; + readQuestionBtn.disabled = false; }; speechSynthesis.speak(currentUtterance); - readQuestionBtn.textContent = '🔊 Reading...'; - readQuestionBtn.disabled = true; - - setTimeout(() => { - readQuestionBtn.textContent = '🔊 Read Question'; - readQuestionBtn.disabled = false; - }, 2000); }); // Activate buzzers activateBuzzerBtn.addEventListener('click', () => { database.ref('gameState').update({ buzzerActive: true, - buzzer: null, - playerAnswer: null + buzzer: null }); activateBuzzerBtn.disabled = true; answerSection.style.display = 'none'; @@ -185,32 +357,66 @@ activateBuzzerBtn.addEventListener('click', () => { markCorrectBtn.addEventListener('click', () => { if (!currentBuzzedPlayer) return; + const points = isShowingBonus ? 10 : 4; + database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => { - return (score || 0) + 4; + return (score || 0) + points; }); - resetForNextQuestion(); + // If tossup was correct, show bonus + if (!isShowingBonus && currentQuestion.bonus_question) { + showBonus(); + } else { + resetForNextQuestion(); + } }); +// Show bonus question +function showBonus() { + isShowingBonus = true; + questionText.textContent = currentQuestion.bonus_question; + questionCategory.textContent = currentQuestion.category + ' - BONUS'; + correctAnswer.textContent = currentQuestion.bonus_answer; + + // Reset UI for bonus + buzzedPlayer.textContent = ''; + document.querySelectorAll('.light').forEach(l => l.classList.remove('active')); + markCorrectBtn.style.display = 'none'; + markIncorrectBtn.style.display = 'none'; + showAnswerBtn.style.display = 'none'; + answerSection.style.display = 'none'; + + readQuestionBtn.disabled = false; + + alert('Tossup correct! Now reading the BONUS question for the same team.'); +} + // Mark incorrect markIncorrectBtn.addEventListener('click', () => { if (!currentBuzzedPlayer) return; - database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => { - return (score || 0) - 4; - }); + const points = isShowingBonus ? 0 : -4; - // Reactivate buzzers for other players - database.ref('gameState').update({ - buzzerActive: true, - buzzer: null, - playerAnswer: null - }); + if (points !== 0) { + database.ref(`gameState/scores/${currentBuzzedPlayer}`).transaction((score) => { + return (score || 0) + points; + }); + } - answerSection.style.display = 'none'; - markCorrectBtn.style.display = 'none'; - markIncorrectBtn.style.display = 'none'; - showAnswerBtn.style.display = 'none'; + if (!isShowingBonus) { + // Reactivate buzzers for tossup + database.ref('gameState').update({ + buzzerActive: true, + buzzer: null + }); + + answerSection.style.display = 'none'; + markCorrectBtn.style.display = 'none'; + markIncorrectBtn.style.display = 'none'; + showAnswerBtn.style.display = 'none'; + } else { + resetForNextQuestion(); + } }); // Show answer @@ -220,9 +426,11 @@ showAnswerBtn.addEventListener('click', () => { // Reset game resetGameBtn.addEventListener('click', () => { - if (confirm('Reset all scores and start over?')) { + if (confirm('Reset all scores and used questions?')) { currentQuestionNumber = 1; + usedQuestionIds.clear(); speechSynthesis.cancel(); + stopTimer(); database.ref('gameState').set({ buzzerActive: false, currentQuestion: null, @@ -237,15 +445,20 @@ resetGameBtn.addEventListener('click', () => { }); questionText.textContent = 'Click "New Question" to start'; answerSection.style.display = 'none'; + readQuestionBtn.disabled = true; + console.log('Game reset! All questions available again.'); } }); function resetForNextQuestion() { - speechSynthesis.cancel(); + if (speechSynthesis.speaking) { + speechSynthesis.cancel(); + } + stopTimer(); + database.ref('gameState').update({ buzzerActive: false, buzzer: null, - playerAnswer: null, isReading: false }); @@ -255,5 +468,8 @@ function resetForNextQuestion() { markIncorrectBtn.style.display = 'none'; showAnswerBtn.style.display = 'none'; answerSection.style.display = 'none'; + readQuestionBtn.textContent = '🔊 Read Question'; + readQuestionBtn.disabled = false; currentBuzzedPlayer = null; + isShowingBonus = false; } \ No newline at end of file diff --git a/player.html b/player.html index a81f72e..d871c54 100644 --- a/player.html +++ b/player.html @@ -26,12 +26,7 @@
- - - diff --git a/player.js b/player.js index 7c2f4d4..3e538b2 100644 --- a/player.js +++ b/player.js @@ -4,14 +4,14 @@ const playerId = urlParams.get('id') || '1'; const playerColors = ['#ff4444', '#4444ff', '#44ff44', '#ffff44']; const playerNames = ['Player 1', 'Player 2', 'Player 3', 'Player 4']; +// Buzzer sound frequencies (different for each player) +const buzzerFrequencies = [523.25, 659.25, 783.99, 880.00]; // C5, E5, G5, A5 + // DOM elements const playerName = document.getElementById('playerName'); const playerScore = document.getElementById('playerScore'); const statusMessage = document.getElementById('statusMessage'); const buzzer = document.getElementById('buzzer'); -const answerContainer = document.getElementById('answerContainer'); -const answerInput = document.getElementById('answerInput'); -const submitAnswer = document.getElementById('submitAnswer'); const questionDisplay = document.getElementById('questionDisplay'); // Set player identity @@ -23,6 +23,26 @@ document.body.style.setProperty('--player-color', playerColors[playerId - 1]); let canBuzz = false; let hasBuzzed = false; +// Audio context for buzzer sounds +const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + +function playBuzzerSound(playerNum) { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = buzzerFrequencies[playerNum - 1]; + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); +} + // Listen to game state database.ref('gameState').on('value', (snapshot) => { const state = snapshot.val(); @@ -33,7 +53,7 @@ database.ref('gameState').on('value', (snapshot) => { playerScore.textContent = state.scores[`player${playerId}`]; } - // Hide question text - players only hear it + // Show question status if (state.isReading) { questionDisplay.textContent = '🔊 Listen to the question...'; questionDisplay.style.display = 'block'; @@ -51,7 +71,6 @@ database.ref('gameState').on('value', (snapshot) => { buzzer.disabled = false; buzzer.classList.remove('locked'); statusMessage.textContent = 'Ready to buzz!'; - answerContainer.style.display = 'none'; } else { canBuzz = false; buzzer.disabled = true; @@ -60,9 +79,8 @@ database.ref('gameState').on('value', (snapshot) => { // Check if this player buzzed in if (state.buzzer && state.buzzer.playerId === `player${playerId}`) { - statusMessage.textContent = 'You buzzed in! Answer now:'; - answerContainer.style.display = 'block'; - answerInput.focus(); + statusMessage.textContent = '🎯 You buzzed in! Answer verbally to the moderator.'; + buzzer.classList.add('locked'); } else if (state.buzzer && state.buzzer.playerId) { const buzzedPlayerNum = state.buzzer.playerId.replace('player', ''); statusMessage.textContent = `Player ${buzzedPlayerNum} buzzed in`; @@ -70,6 +88,15 @@ database.ref('gameState').on('value', (snapshot) => { } }); +// Listen for buzzer events to play sounds +database.ref('gameState/buzzer').on('value', (snapshot) => { + const buzzer = snapshot.val(); + if (buzzer && buzzer.playerId) { + const playerNum = parseInt(buzzer.playerId.replace('player', '')); + playBuzzerSound(playerNum); + } +}); + // Buzzer click buzzer.addEventListener('click', buzzIn); @@ -93,25 +120,4 @@ function buzzIn() { // Visual feedback buzzer.classList.add('buzzed'); setTimeout(() => buzzer.classList.remove('buzzed'), 300); -} - -// Submit answer -submitAnswer.addEventListener('click', submitPlayerAnswer); -answerInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') submitPlayerAnswer(); -}); - -function submitPlayerAnswer() { - const answer = answerInput.value.trim(); - if (!answer) return; - - database.ref('gameState/playerAnswer').set({ - playerId: `player${playerId}`, - answer: answer, - timestamp: Date.now() - }); - - answerInput.value = ''; - answerContainer.style.display = 'none'; - statusMessage.textContent = 'Answer submitted! Waiting for moderator...'; } \ No newline at end of file diff --git a/styles.css b/styles.css index 3c7f601..6bdb714 100644 --- a/styles.css +++ b/styles.css @@ -372,4 +372,69 @@ margin-top: 15px; } .instructions li { margin: 10px 0; +} +.timer-display { + font-size: 3em; + font-weight: bold; + color: #ffaa00; + text-align: center; + margin: 20px 0; + animation: pulse 1s infinite; +} + +.settings-section { + background: rgba(255,255,255,0.1); + padding: 20px; + border-radius: 15px; + margin-bottom: 20px; +} + +.setting-group { + margin-bottom: 20px; +} + +.setting-group:last-child { + margin-bottom: 0; +} + +.setting-group h3 { + margin-bottom: 15px; + color: #ffff44; +} + +.source-select { + width: 100%; + padding: 12px; + font-size: 1.1em; + border-radius: 8px; + border: 2px solid rgba(255,255,255,0.3); + background: rgba(255,255,255,0.1); + color: white; + margin-bottom: 10px; + cursor: pointer; +} + +.source-select option { + background: #1a1a2e; + color: white; +} + +.round-input { + width: 100%; + padding: 12px; + font-size: 1.1em; + border-radius: 8px; + border: 2px solid rgba(255,255,255,0.3); + background: rgba(255,255,255,0.1); + color: white; +} + +.round-input::placeholder { + color: rgba(255,255,255,0.5); +} + +.category-filter { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; } \ No newline at end of file