/** * Socket Client Module * Handles WebSocket communication for receiving text fragments and game state */ import { BaseModule } from './base-module.js'; class SocketClientModule extends BaseModule { constructor() { super('socket-client', 'Socket Client'); // Dependencies this.dependencies = ['text-buffer']; this.socket = null; this.textBuffer = null; this.isConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 2000; this.url = null; this.eventListeners = {}; this.defaultHost = 'localhost:3000'; // Bind methods using parent's bindMethods utility this.bindMethods([ 'connect', 'disconnect', 'send', 'sendCommand', 'requestStartGame', 'requestSaveGame', 'requestLoadGame', 'on', 'off', 'emitEvent', 'setupGameEventHandlers', 'processTextFragment', 'attemptReconnect', 'getConnectionStatus', 'loadSocketIO' ]); } /** * Load Socket.IO client library * @returns {Promise} */ loadSocketIO() { // Use parent's loadScript method return this.loadScript('/socket.io/socket.io.js'); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, "Initializing Socket Client"); // Dynamically load Socket.IO client if not already loaded if (!window.io) { this.reportProgress(20, "Loading Socket.IO client"); await this.loadSocketIO(); this.reportProgress(30, "Socket.IO client loaded"); } // Get text buffer using parent's getModule method this.textBuffer = this.getModule('text-buffer'); if (!this.textBuffer) { console.error("Socket Client: Failed to get text-buffer module"); return false; } this.reportProgress(50, "Setting up connection parameters"); // Use the current origin for the socket connection const currentUrl = window.location.origin; console.log(`Socket Client: Using origin for connection: ${currentUrl}`); this.url = currentUrl; this.reportProgress(100, "Socket client initialized"); return true; } catch (error) { console.error("Error initializing Socket Client:", error); return false; } } /** * Connect to the Socket.IO server * @param {string} url - Optional custom WebSocket URL * @returns {Promise} - Resolves with connection success */ connect(url = null) { return new Promise((resolve) => { if (this.isConnected) { resolve(true); return; } // Use provided URL or default const socketUrl = url || this.url; try { console.log(`Socket Client: Connecting to ${socketUrl}`); // Create Socket.IO connection (will automatically use /socket.io endpoint) this.socket = window.io(socketUrl, { reconnection: false, // We handle reconnection ourselves transports: ['websocket', 'polling'] // Prefer WebSocket }); this.socket.on('connect', () => { console.log('Socket Client: Connected to server with ID:', this.socket.id); this.isConnected = true; this.reconnectAttempts = 0; this.emitEvent('connect'); resolve(true); }); this.socket.on('disconnect', (reason) => { console.log(`Socket Client: Connection closed: ${reason}`); this.isConnected = false; this.emitEvent('disconnect', reason); this.attemptReconnect(); resolve(false); }); this.socket.on('connect_error', (error) => { console.error('Socket Client: Connection error:', error); this.emitEvent('connect_error', error); resolve(false); }); // Set up game-specific event handlers this.setupGameEventHandlers(); } catch (error) { console.error('Socket Client: Connection error:', error); this.emitEvent('connect_error', error); resolve(false); } }); } /** * Set up event handlers for game-specific Socket.IO events */ setupGameEventHandlers() { if (!this.socket) return; // Map all incoming Socket.IO events to our internal event system this.socket.onAny((event, ...args) => { console.log(`Socket Client: Received ${event} event from server`, args); this.emitEvent(event, args[0]); }); // Special handling for narrative text this.socket.on('narrativeResponse', (data) => { if (data && data.text && this.textBuffer) { this.processTextFragment(data.text); } }); // Special handling for introduction text this.socket.on('gameIntroduction', (data) => { if (data && data.introduction && this.textBuffer) { this.processTextFragment(data.introduction); } if (data && data.initialRoomDescription && this.textBuffer) { this.processTextFragment(data.initialRoomDescription); } }); } /** * Process a text fragment by adding it to the TextBuffer * @param {string} text - Text fragment to process */ processTextFragment(text) { if (!text) return; // Add text to the buffer if available if (this.textBuffer) { console.log(`Socket Client: Processing text fragment: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`); this.textBuffer.addText(text); } else { console.error('Socket Client: Text buffer not available'); // Attempt to get text buffer again using parent's getModule method this.textBuffer = this.getModule('text-buffer'); if (this.textBuffer) { this.textBuffer.addText(text); } else { // Emit a text event as fallback if no text buffer this.emitEvent('text', text); } } } /** * Attempt to reconnect to the server */ attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Socket Client: Max reconnect attempts reached'); return; } this.reconnectAttempts++; const delay = this.reconnectDelay * this.reconnectAttempts; console.log(`Socket Client: Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => { if (!this.isConnected) { this.connect(); } }, delay); } /** * Disconnect from the server */ disconnect() { if (this.socket && this.isConnected) { this.socket.disconnect(); this.isConnected = false; } } /** * Send a message to the server * @param {Object|string} data - Data to send * @returns {boolean} - Success status */ send(data) { if (!this.isConnected || !this.socket) { console.error('Socket Client: Not connected'); return false; } try { // For Socket.IO we send structured events if (typeof data === 'object') { const { type, ...restData } = data; if (type) { // Use the type as the event name this.socket.emit(type, restData); } else { // Default to 'message' event this.socket.emit('message', data); } } else { // Plain strings go to 'message' event this.socket.emit('message', { text: data }); } return true; } catch (error) { console.error('Socket Client: Error sending message:', error); return false; } } /** * Send a command to the server * @param {string} command - The player's command * @returns {boolean} - Success status */ sendCommand(command) { if (!this.isConnected || !this.socket) { console.error('Socket Client: Not connected, cannot send command'); return false; } try { this.socket.emit('playerCommand', { command }); return true; } catch (error) { console.error('Socket Client: Error sending command:', error); return false; } } /** * Request to start a new game * @returns {boolean} - Success status */ requestStartGame() { if (!this.isConnected || !this.socket) { console.error('Socket Client: Not connected, cannot start game'); return false; } try { this.socket.emit('startGame'); return true; } catch (error) { console.error('Socket Client: Error starting game:', error); return false; } } /** * Request to save the current game state * @returns {boolean} - Success status */ requestSaveGame() { if (!this.isConnected || !this.socket) { console.error('Socket Client: Not connected, cannot save game'); return false; } try { this.socket.emit('saveGame'); return true; } catch (error) { console.error('Socket Client: Error saving game:', error); return false; } } /** * Request to load a saved game state * @returns {boolean} - Success status */ requestLoadGame() { if (!this.isConnected || !this.socket) { console.error('Socket Client: Not connected, cannot load game'); return false; } try { this.socket.emit('loadGame'); return true; } catch (error) { console.error('Socket Client: Error loading game:', error); return false; } } /** * Register an event handler * @param {string} event - Event name * @param {Function} callback - Event callback */ on(event, callback) { if (!this.eventListeners[event]) { this.eventListeners[event] = []; } this.eventListeners[event].push(callback); } /** * Remove an event handler * @param {string} event - Event name * @param {Function} callback - Event callback to remove */ off(event, callback) { if (!this.eventListeners[event]) return; if (callback) { // Remove specific callback this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback); } else { // Remove all callbacks for this event delete this.eventListeners[event]; } } /** * Emit an event to all registered listeners * @param {string} event - Event name * @param {*} data - Event data */ emitEvent(event, data) { if (!this.eventListeners[event]) return; for (const callback of this.eventListeners[event]) { try { callback(data); } catch (error) { console.error(`Socket Client: Error in '${event}' event handler:`, error); } } } /** * Check if the socket is connected * @returns {boolean} - Connection status */ getConnectionStatus() { return this.isConnected; } } // Create the singleton instance const SocketClient = new SocketClientModule(); // Export the module export { SocketClient };