/* ═══════════════════════════════════════ RIDDLE RACER – client-side game logic ═══════════════════════════════════════ */ const socket = io(); const FINISH_LINE = 10; const CAR_EMOJIS = ['🏎️', '🚗', '🚕', '🚙', '🚓', '🚑', '🚐', '🚌']; // ── State ────────────────────────────── let myRoomId = null; let amHost = false; let myFinished = false; let carEmojiMap = {}; // playerId → car emoji // ── Screen helpers ───────────────────── const screens = { home: document.getElementById('home-screen'), lobby: document.getElementById('lobby-screen'), game: document.getElementById('game-screen'), results: document.getElementById('results-screen'), }; function showScreen(name) { Object.values(screens).forEach(s => s.classList.remove('active')); screens[name].classList.add('active'); } // ── DOM refs ─────────────────────────── const nameInput = document.getElementById('name-input'); const codeInput = document.getElementById('code-input'); const errorMsg = document.getElementById('error-msg'); const createBtn = document.getElementById('create-btn'); const joinBtn = document.getElementById('join-btn'); const roomCodeEl = document.getElementById('room-code'); const copyBtn = document.getElementById('copy-btn'); const playerListEl = document.getElementById('player-list'); const playerCount = document.getElementById('player-count'); const startBtn = document.getElementById('start-btn'); const waitingMsg = document.getElementById('waiting-msg'); const gameBadge = document.getElementById('game-room-badge'); const raceTrack = document.getElementById('race-track'); const riddlePanel = document.getElementById('riddle-panel'); const riddleNumEl = document.getElementById('riddle-num'); const riddleTotEl = document.getElementById('riddle-total'); const progressFill = document.getElementById('progress-fill'); const riddleQ = document.getElementById('riddle-question'); const answerInput = document.getElementById('answer-input'); const submitBtn = document.getElementById('submit-btn'); const hintBtn = document.getElementById('hint-btn'); const feedbackEl = document.getElementById('feedback'); const hintBox = document.getElementById('hint-box'); const hintText = document.getElementById('hint-text'); const finishedBanner = document.getElementById('finished-banner'); const finishedTrophy = document.getElementById('finished-trophy'); const finishedText = document.getElementById('finished-text'); const resultsList = document.getElementById('results-list'); const playAgainBtn = document.getElementById('play-again-btn'); const hostOnlyMsg = document.getElementById('host-only-msg'); // ══════════════════════════════════════════════ // HOME SCREEN // ══════════════════════════════════════════════ // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`${tab}-tab`).classList.add('active'); clearError(); }); }); createBtn.addEventListener('click', () => { const name = nameInput.value.trim(); if (!name) return showError('Please enter your racing name first.'); clearError(); createBtn.disabled = true; socket.emit('createRoom', { name }); }); joinBtn.addEventListener('click', () => { const name = nameInput.value.trim(); const code = codeInput.value.trim().toUpperCase(); if (!name) return showError('Please enter your racing name first.'); if (!code) return showError('Please enter the room code.'); clearError(); joinBtn.disabled = true; socket.emit('joinRoom', { name, code }); }); nameInput.addEventListener('keydown', e => { if (e.key === 'Enter') createBtn.click(); }); codeInput.addEventListener('keydown', e => { if (e.key === 'Enter') joinBtn.click(); }); codeInput.addEventListener('input', () => { codeInput.value = codeInput.value.toUpperCase(); }); function showError(msg) { errorMsg.textContent = msg; } function clearError() { errorMsg.textContent = ''; } function resetHomeButtons() { createBtn.disabled = false; joinBtn.disabled = false; } // ══════════════════════════════════════════════ // LOBBY SCREEN // ══════════════════════════════════════════════ copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(myRoomId).then(() => showNotif('Room code copied! 📋')); }); startBtn.addEventListener('click', () => { socket.emit('startGame'); }); function renderLobbyPlayers(players) { playerCount.textContent = players.length; playerListEl.innerHTML = players.map((p, i) => { const isMe = p.id === socket.id; const isHost = i === 0; // first player is host (by join order) return `
${escHtml(p.name)} ${isHost ? 'Host' : ''} ${isMe ? 'You' : ''}
`; }).join(''); } // ══════════════════════════════════════════════ // GAME SCREEN // ══════════════════════════════════════════════ // Assign a car emoji to each player consistently function assignCarEmojis(players) { players.forEach((p, i) => { if (!carEmojiMap[p.id]) { carEmojiMap[p.id] = CAR_EMOJIS[i % CAR_EMOJIS.length]; } }); } function renderTrack(players) { assignCarEmojis(players); raceTrack.innerHTML = players.map(p => { const pct = Math.min((p.position / FINISH_LINE) * 88 + 4, 92); const carEmoji = carEmojiMap[p.id] || '🏎️'; const isMe = p.id === socket.id; return `
${isMe ? '▶ ' : ''}${escHtml(p.name)}
${carEmoji}
🏁 ${p.finishPosition ? `${getOrdinal(p.finishPosition)}` : ''}
`; }).join(''); } function updateTrack(players) { assignCarEmojis(players); players.forEach(p => { const carEl = document.getElementById(`car-${p.id}`); if (!carEl) return; const pct = Math.min((p.position / FINISH_LINE) * 88 + 4, 92); const oldLeft = parseFloat(carEl.style.left); if (Math.abs(pct - oldLeft) > 0.1) { carEl.style.left = `${pct}%`; // Trigger boost animation carEl.classList.remove('boosting'); void carEl.offsetWidth; // reflow carEl.classList.add('boosting'); setTimeout(() => carEl.classList.remove('boosting'), 450); } // Update place badge const row = document.querySelector(`.track-row[data-player-id="${p.id}"]`); if (row) { const finishCol = row.querySelector('.finish-col'); const existing = finishCol.querySelector('.place-badge'); if (p.finishPosition && !existing) { finishCol.insertAdjacentHTML('beforeend', `${getOrdinal(p.finishPosition)}`); } } }); } function showRiddle(riddle, num, total) { riddleNumEl.textContent = num; riddleTotEl.textContent = total; riddleQ.textContent = riddle.question; progressFill.style.width = `${((num - 1) / total) * 100}%`; answerInput.value = ''; feedbackEl.textContent = ''; feedbackEl.className = 'feedback'; hintBox.style.display = 'none'; hintText.textContent = ''; answerInput.focus(); } function setFeedback(msg, isCorrect) { feedbackEl.textContent = msg; feedbackEl.className = `feedback ${isCorrect ? 'correct' : 'wrong'}`; if (!isCorrect) { // Shake the input answerInput.style.borderColor = 'var(--accent-red)'; setTimeout(() => answerInput.style.borderColor = '', 700); } } submitBtn.addEventListener('click', submitAnswer); answerInput.addEventListener('keydown', e => { if (e.key === 'Enter') submitAnswer(); }); function submitAnswer() { if (myFinished) return; const answer = answerInput.value.trim(); if (!answer) return; socket.emit('submitAnswer', { answer }); submitBtn.disabled = true; setTimeout(() => { submitBtn.disabled = false; }, 300); } hintBtn.addEventListener('click', () => { socket.emit('getHint'); }); // ══════════════════════════════════════════════ // RESULTS SCREEN // ══════════════════════════════════════════════ playAgainBtn.addEventListener('click', () => { if (amHost) { socket.emit('playAgain'); } else { showNotif('Only the host can restart the race!'); } }); function renderResults(results) { const medals = ['🥇', '🥈', '🥉']; resultsList.innerHTML = results.map((r, i) => `
${i < 3 ? `${medals[i]}` : `${r.place}.` } ${escHtml(r.name)}
`).join(''); if (amHost) { playAgainBtn.style.display = 'block'; hostOnlyMsg.style.display = 'none'; } else { playAgainBtn.style.display = 'none'; hostOnlyMsg.style.display = 'block'; } } // ══════════════════════════════════════════════ // SOCKET EVENTS // ══════════════════════════════════════════════ socket.on('roomCreated', ({ roomId }) => { myRoomId = roomId; amHost = true; resetHomeButtons(); }); socket.on('joinedRoom', ({ roomId }) => { myRoomId = roomId; amHost = false; resetHomeButtons(); }); socket.on('joinError', msg => { showError(msg); resetHomeButtons(); }); socket.on('lobbyState', ({ players, isHost, roomId }) => { myRoomId = roomId; amHost = isHost; myFinished = false; carEmojiMap = {}; roomCodeEl.textContent = roomId; startBtn.style.display = isHost ? 'block' : 'none'; waitingMsg.style.display = isHost ? 'none' : 'block'; renderLobbyPlayers(players); showScreen('lobby'); }); socket.on('playerUpdate', ({ players, message, newHost }) => { if (newHost && newHost === socket.id) { amHost = true; startBtn.style.display = 'block'; waitingMsg.style.display = 'none'; } renderLobbyPlayers(players); if (message) showNotif(message); }); socket.on('gameStarted', ({ players, riddle, riddleNum, total }) => { myFinished = false; gameBadge.textContent = myRoomId; // Reset game UI riddlePanel.style.display = 'block'; finishedBanner.style.display = 'none'; assignCarEmojis(players); renderTrack(players); showRiddle(riddle, riddleNum, total); showScreen('game'); }); socket.on('positionUpdate', ({ players }) => { updateTrack(players); }); socket.on('answerResult', ({ correct, finished, finishPosition, riddle, riddleNum, total }) => { if (!correct) { setFeedback('✗ Wrong! Try again.', false); return; } if (finished) { myFinished = true; progressFill.style.width = '100%'; riddlePanel.style.display = 'none'; finishedBanner.style.display = 'flex'; const trophies = ['🏆', '🥈', '🥉', '🎖️']; finishedTrophy.textContent = trophies[Math.min(finishPosition - 1, trophies.length - 1)] || '🎖️'; finishedText.textContent = `You finished ${getOrdinal(finishPosition)}!`; } else { setFeedback('✓ Correct! Keep going!', true); setTimeout(() => { if (!myFinished) showRiddle(riddle, riddleNum, total); }, 600); } }); socket.on('playerFinished', ({ name, place }) => { showNotif(`🏁 ${escHtml(name)} finished ${getOrdinal(place)}!`, true); }); socket.on('hint', ({ hint }) => { hintText.textContent = hint; hintBox.style.display = 'block'; }); socket.on('gameOver', ({ results }) => { renderResults(results); showScreen('results'); }); socket.on('returnToLobby', ({ players }) => { myFinished = false; carEmojiMap = {}; startBtn.style.display = amHost ? 'block' : 'none'; waitingMsg.style.display = amHost ? 'none' : 'block'; renderLobbyPlayers(players); showScreen('lobby'); }); socket.on('disconnect', () => { showNotif('Connection lost. Refresh to reconnect.'); }); socket.on('connect_error', () => { showError('Could not connect to server.'); }); // ══════════════════════════════════════════════ // UTILITIES // ══════════════════════════════════════════════ function showNotif(msg, isFinish = false) { const container = document.getElementById('notif-container'); const el = document.createElement('div'); el.className = `notif${isFinish ? ' finish-notif' : ''}`; el.textContent = msg; container.appendChild(el); setTimeout(() => el.remove(), 3200); } function getOrdinal(n) { const s = ['th', 'st', 'nd', 'rd']; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } function escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }