// overlay.js - WebSocket client for real-time achievement updates class RetroPulseOverlay { constructor() { this.ws = null; this.username = null; this.reconnectInterval = null; this.reconnectDelay = 3000; // 3 seconds this.lastUpdateTime = null; // DOM elements this.connectionStatus = document.getElementById('connection-status'); this.currentGame = document.getElementById('current-game'); this.gameIcon = document.getElementById('game-icon'); this.gameTitle = document.getElementById('game-title'); this.consoleName = document.getElementById('console-name'); this.achievementsList = document.getElementById('achievements-list'); this.errorDisplay = document.getElementById('error-display'); this.errorMessage = document.getElementById('error-message'); this.init(); } init() { // Extract username from URL path const pathParts = window.location.pathname.split('/'); this.username = pathParts[pathParts.length - 1]; if (!this.username || this.username === 'overlay') { this.showError('No username specified in URL'); return; } console.log(`🎮 Initializing RetroPulse overlay for user: ${this.username}`); this.connect(); } connect() { try { // Determine WebSocket URL (ws:// for http, wss:// for https) const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws?user=${encodeURIComponent(this.username)}`; console.log(`🔌 Connecting to WebSocket: ${wsUrl}`); this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('✅ WebSocket connected'); this.updateConnectionStatus('connected'); // Clear reconnect interval if it exists if (this.reconnectInterval) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleMessage(message); } catch (error) { console.error('Error parsing WebSocket message:', error); } }; this.ws.onclose = (event) => { console.log('❌ WebSocket disconnected:', event.code, event.reason); this.updateConnectionStatus('disconnected'); // Don't reconnect if it was a clean close if (event.code !== 1000) { this.scheduleReconnect(); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.updateConnectionStatus('disconnected'); }; } catch (error) { console.error('Error creating WebSocket connection:', error); this.showError(`Connection failed: ${error.message}`); } } handleMessage(message) { console.log('📨 Received message:', message); if (message.error) { this.showError(message.error); return; } if (message.type === 'initial' || message.type === 'update') { this.updateOverlay(message.data); } } updateOverlay(data) { if (!data) return; console.log('🔄 Updating overlay with data:', data); // Update last update time this.lastUpdateTime = new Date(); // Hide error display this.errorDisplay.classList.add('hidden'); // Update achievements if (data.recentAchievements && data.recentAchievements.length > 0) { this.updateAchievements(data.recentAchievements); // Update game info from the latest achievement const latestAchievement = data.recentAchievements[0]; if (latestAchievement.gameTitle) { this.updateGameInfo({ title: latestAchievement.gameTitle, icon: latestAchievement.gameIcon }); } } else { // No achievements yet this.achievementsList.innerHTML = '
No recent achievements
'; } } updateGameInfo(gameData) { if (!gameData) return; this.gameTitle.textContent = gameData.title || 'Unknown Game'; if (gameData.icon) { this.gameIcon.src = gameData.icon; this.gameIcon.onerror = () => { this.gameIcon.style.display = 'none'; }; } this.currentGame.classList.remove('hidden'); } updateAchievements(achievements) { // Clear existing achievements this.achievementsList.innerHTML = ''; achievements.forEach((achievement, index) => { const achievementElement = this.createAchievementElement(achievement); // Add a small delay for staggered animation setTimeout(() => { this.achievementsList.appendChild(achievementElement); }, index * 100); }); } createAchievementElement(achievement) { const item = document.createElement('div'); item.className = 'achievement-item'; // Format the date const dateEarned = achievement.dateEarned ? new Date(achievement.dateEarned).toLocaleDateString() : 'Recently'; item.innerHTML = ` Achievement Badge
${this.escapeHtml(achievement.title || 'Unknown Achievement')}
${this.escapeHtml(achievement.description || '')}
${achievement.points || 0} pts ${dateEarned}
`; return item; } updateConnectionStatus(status) { this.connectionStatus.className = `connection-status ${status}`; const statusTexts = { connecting: 'Connecting...', connected: 'Live', disconnected: 'Disconnected' }; this.connectionStatus.querySelector('.status-text').textContent = statusTexts[status] || status; } scheduleReconnect() { if (this.reconnectInterval) return; console.log(`⏰ Scheduling reconnect in ${this.reconnectDelay}ms`); this.reconnectInterval = setTimeout(() => { console.log('🔄 Attempting to reconnect...'); this.connect(); }, this.reconnectDelay); // Increase delay for next reconnect (max 30 seconds) this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 30000); } showError(message) { console.error('❌ Overlay error:', message); this.errorMessage.textContent = message; this.errorDisplay.classList.remove('hidden'); this.updateConnectionStatus('disconnected'); } // Utility function to escape HTML escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, (m) => map[m]); } } // Initialize overlay when page loads document.addEventListener('DOMContentLoaded', () => { new RetroPulseOverlay(); }); // Handle page visibility for OBS document.addEventListener('visibilitychange', () => { if (!document.hidden) { console.log('🔄 Page became visible, overlay active'); } });