// Multiplayer Client Manager class MultiplayerClient { constructor(game) { this.game = game; this.socket = null; this.playerId = null; this.username = null; this.currentGameId = null; this.isMultiplayer = false; this.activePlayers = []; this.pendingChallenges = new Map(); this.selectedBoardSize = 15; // Default board size for multiplayer } // Connect to server async connect() { if (typeof io === 'undefined') { this.showMessage('Socket.io library not loaded. Please check your internet connection.', 'error'); return; } // Show username modal immediately if not saved const savedUsername = localStorage.getItem('connect5_username'); if (!savedUsername) { this.showUsernameModal(); } else { this.username = savedUsername; // Pre-load username } // Dynamically construct proxy URLs based on current origin const targetUrl = window.location.origin; const servers = [ targetUrl, // Primary (Production/Local) 'http://localhost:3000' // Failover for local dev ]; let connected = false; const loadingEl = document.querySelector('.loading'); for (const serverUrl of servers) { if (connected) break; try { if (loadingEl) loadingEl.textContent = `Connecting to ${serverUrl}...`; console.log(`Attempting connection to: ${serverUrl}`); await this.tryConnect(serverUrl); connected = true; console.log(`✅ Successfully connected to: ${serverUrl}`); if (loadingEl) loadingEl.textContent = 'Connected! verifying functionality...'; } catch (error) { console.warn(`❌ Failed to connect to ${serverUrl}:`, error); } } if (!connected) { this.showMessage('Failed to connect to any multiplayer server. Please try again later.', 'error'); const loading = document.querySelector('.loading'); if (loading) loading.textContent = 'Connection failed.'; } // Setup board size selector buttons document.querySelectorAll('.size-btn-mp').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.size-btn-mp').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); this.selectedBoardSize = parseInt(e.target.dataset.size); }); }); } tryConnect(url) { return new Promise((resolve, reject) => { const socketOptions = { reconnection: false, // We handle reconnection manually for failover timeout: 5000, transports: ['websocket', 'polling'] }; const tempSocket = io(url, socketOptions); const timeout = setTimeout(() => { if (!tempSocket.connected) { tempSocket.close(); reject(new Error('Connection timed out')); } }, 5000); tempSocket.on('connect', () => { clearTimeout(timeout); this.socket = tempSocket; this.setupSocketListeners(); resolve(); }); tempSocket.on('connect_error', (err) => { clearTimeout(timeout); tempSocket.close(); reject(err); }); }); } setupSocketListeners() { if (!this.socket) return; // Safety timeout: If we are connected but don't get a player list or login prompt within 5 seconds, warn the user. setTimeout(() => { const loading = document.querySelector('.loading'); if (loading && loading.textContent.includes('Connecting')) { loading.textContent = 'Connection successful, but server response is slow...'; } else if (loading && loading.textContent === 'Loading players...') { loading.innerHTML = 'Server not responding. Retry'; } }, 5000); this.socket.on('connect', () => { console.log('✅ Connected to multiplayer server'); // If we have a username (from localStorage or recently entered), try to register const savedUsername = localStorage.getItem('connect5_username') || this.username; if (savedUsername) { console.log('Found saved username:', savedUsername); this.registerPlayer(savedUsername); } // If no username yet, do nothing (user is seeing the modal and will call registerPlayer when they submit) }); this.socket.on('disconnect', () => { console.log('❌ Disconnected from server'); this.handleDisconnect(); }); this.socket.on('registration_result', (data) => { this.handleRegistration(data); }); this.socket.on('active_players_update', (players) => { this.updateActivePlayers(players); }); this.socket.on('challenge_received', (data) => { this.showChallengeNotification(data); }); this.socket.on('challenge_result', (data) => { this.handleChallengeResult(data); }); this.socket.on('challenge_declined', (data) => { this.showMessage(`${data.by} declined your challenge`, 'error'); }); this.socket.on('game_started', (data) => { this.startMultiplayerGame(data); }); this.socket.on('opponent_move', (data) => { this.handleOpponentMove(data); }); this.socket.on('move_result', (data) => { this.handleMoveResult(data); }); this.socket.on('game_ended', (data) => { this.handleGameEnded(data); }); this.socket.on('opponent_disconnected', (data) => { this.showMessage(data.message + '. Waiting for reconnection...', 'warning'); }); // Send heartbeat every 30 seconds setInterval(() => { if (this.socket && this.socket.connected) { this.socket.emit('heartbeat'); } }, 30000); } // Show username input modal showUsernameModal() { const modal = document.getElementById('usernameModal'); if (modal) { modal.classList.add('active'); } } // Register player registerPlayer(username) { this.username = username; localStorage.setItem('connect5_username', username); // Hide username modal immediately for better UX const modal = document.getElementById('usernameModal'); if (modal) { modal.classList.remove('active'); } // Show loading state const loading = document.querySelector('.loading'); if (loading) { loading.textContent = 'Registering...'; } if (!this.socket || !this.socket.connected) { console.log('Socket not ready yet, username saved and will be sent on connect.'); if (loading) { loading.textContent = 'Connecting to server...'; } return; } console.log('Emitting register_player for:', username); this.socket.emit('register_player', { username }); } // Handle registration response handleRegistration(data) { if (data.success) { this.playerId = data.player.id; this.username = data.player.username; // Save username to localStorage for auto-login localStorage.setItem('connect5_username', this.username); console.log('Username saved to localStorage'); // Hide username modal (if visible) const modal = document.getElementById('usernameModal'); if (modal) { modal.classList.remove('active'); } // Show multiplayer panel const multiplayerPanel = document.getElementById('multiplayerPanel'); if (multiplayerPanel) { multiplayerPanel.style.display = 'block'; } // Update player stats display document.getElementById('playerUsername').textContent = this.username; document.getElementById('playerWins').textContent = data.player.stats.wins; document.getElementById('playerLosses').textContent = data.player.stats.losses; document.getElementById('playerDraws').textContent = data.player.stats.draws; this.showMessage(`Welcome back, ${this.username}!`, 'success'); // Request active players this.socket.emit('request_active_players'); } else { // Registration failed - clear saved username and show modal localStorage.removeItem('connect5_username'); this.showMessage(data.error, 'error'); this.showUsernameModal(); } } // Update active players list updateActivePlayers(players) { this.activePlayers = players.filter(p => p.username !== this.username); const container = document.getElementById('activePlayersList'); if (!container) return; if (this.activePlayers.length === 0) { container.innerHTML = '
${data.from} wants to play
Board size: ${data.boardSize}×${data.boardSize}