import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import { io } from 'socket.io-client'; import { API_CONFIG } from '../api/userApi'; const isDev = import.meta.env.DEV; const log = (...args) => isDev && console.log(...args); const warn = (...args) => isDev && console.warn(...args); const logError = (...args) => console.error(...args); /** * Optimized WebSocket hook for game connection * @param {string} gameToken - JWT token from game join * @returns {Object} WebSocket state and methods */ export const useGameWebSocket = (gameToken) => { const socketRef = useRef(null); const [isConnected, setIsConnected] = useState(false); const [gameState, setGameState] = useState(null); const [boardData, setBoardData] = useState(null); const [error, setError] = useState(null); const [isGamemaster, setIsGamemaster] = useState(false); const [gameStarted, setGameStarted] = useState(false); const [pendingPlayers, setPendingPlayers] = useState([]); // Players waiting for approval const [approvalStatus, setApprovalStatus] = useState(null); // 'pending' | 'approved' | 'denied' | null const eventListenersRef = useRef(new Map()); // Memoized derived values - no extra state needed const players = useMemo(() => { // Backend sends different player fields depending on game state // connectedPlayers: array of player names (strings) who are connected via WebSocket // players: array of player IDs (UUIDs) - NOT USEFUL for display // currentPlayers: full player objects with game data (positions, etc.) const connectedPlayers = gameState?.connectedPlayers || []; const currentPlayers = gameState?.currentPlayers || []; // Debug: log what we received if (import.meta.env.DEV) { console.log('๐ŸŽฎ Computing players list:'); console.log(' - connectedPlayers:', connectedPlayers); console.log(' - currentPlayers:', currentPlayers); } // If we have full player objects with positions, use those (during game) if (currentPlayers.length > 0) { console.log('โœ… Using currentPlayers'); return currentPlayers; } // Otherwise, use connectedPlayers (player names from Redis) if (connectedPlayers.length > 0) { console.log('โœ… Mapping connectedPlayers to player objects'); return connectedPlayers.map((nameOrObj, index) => { // Handle both string names and objects const playerName = typeof nameOrObj === 'string' ? nameOrObj : (nameOrObj.playerName || nameOrObj.name || `Player ${index + 1}`); return { id: `player-${index}`, name: playerName, isOnline: true, isReady: gameState?.readyPlayers?.includes(playerName) || false, }; }); } console.log('โš ๏ธ No players found'); return []; }, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]); const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]); // Connect to game WebSocket - only once per token useEffect(() => { if (!gameToken) return; log('๐Ÿ”Œ Connecting to game WebSocket...'); // Connect to /game namespace socketRef.current = io(`${API_CONFIG.wsURL}/game`, { transports: ['websocket'], reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, timeout: 5000, }); const socket = socketRef.current; // Connection handlers const handleConnect = () => { log('โœ… Connected to game WebSocket'); setIsConnected(true); setError(null); socket.emit('game:join', { gameToken }); }; const handleConnectError = (err) => { error('โŒ Connection error:', err); setIsConnected(false); setError(err.message); }; const handleDisconnect = (reason) => { log('๐Ÿ”Œ Disconnected:', reason); setIsConnected(false); }; // Game state handlers - batch updates const handleGameState = (state) => { log('๐Ÿ“Š Game state received:', state); log(' - connectedPlayers:', state?.connectedPlayers); log(' - players:', state?.players); log(' - currentPlayers:', state?.currentPlayers); log(' - isGamemaster in state:', state?.isGamemaster); // EXTRA DEBUG: Show full state structure if (import.meta.env.DEV) { console.log('๐Ÿ” FULL STATE OBJECT:', JSON.stringify(state, null, 2)); } // If state contains isGamemaster flag, update it if (state?.isGamemaster !== undefined) { log('โœ… Setting isGamemaster from state:', state.isGamemaster); setIsGamemaster(state.isGamemaster); } setGameState(state); }; const handleGameJoined = (data) => { log('โœ… Joined game:', data); // EXTRA DEBUG: Show full joined data if (import.meta.env.DEV) { console.log('๐Ÿ” FULL JOINED DATA:', JSON.stringify(data, null, 2)); } // Store if this user is the gamemaster if (data.isGamemaster !== undefined) { log('โœ… Setting isGamemaster from joined event:', data.isGamemaster); setIsGamemaster(data.isGamemaster); } else { log('โš ๏ธ No isGamemaster flag in joined event'); } // Backend will send game:state next }; const handlePlayerJoined = (data) => { log('๐Ÿ‘ค Player joined:', data.playerName); // EXTRA DEBUG if (import.meta.env.DEV) { console.log('๐Ÿ” PLAYER JOINED EVENT:', JSON.stringify(data, null, 2)); } // Update game state to add the new player to connectedPlayers setGameState(prev => { if (!prev) { log('โš ๏ธ No previous game state, cannot add player'); return prev; } const currentConnected = prev.connectedPlayers || []; // Only add if not already in the list if (!currentConnected.includes(data.playerName)) { log('โœ… Adding player to connectedPlayers:', data.playerName); log(' - Current list:', currentConnected); log(' - New list:', [...currentConnected, data.playerName]); return { ...prev, connectedPlayers: [...currentConnected, data.playerName] }; } log('โš ๏ธ Player already in connectedPlayers:', data.playerName); return prev; }); }; const handleGameStarted = (data) => { log('๐ŸŽฎ Game started:', data); // EXTRA DEBUG if (import.meta.env.DEV) { console.log('๐Ÿ” GAME STARTED EVENT:', JSON.stringify(data, null, 2)); } // Signal that game has started setGameStarted(true); // Request updated game state from server (includes boardData and currentPlayers) const socket = socketRef.current; if (socket && socket.connected) { log('๐Ÿ“ก Requesting updated game state after game start'); // The server will send game:state event with full data } }; const handlePlayerMoved = (moveData) => { log('๐Ÿƒ Player moved:', moveData.playerName); // Update only the moved player setGameState(prev => { if (!prev?.currentPlayers) return prev; return { ...prev, currentPlayers: prev.currentPlayers.map(p => p.playerId === moveData.playerId ? { ...p, boardPosition: moveData.newPosition } : p ), }; }); }; const handleTurnChanged = (data) => { log('๐Ÿ”„ Turn changed to:', data.currentPlayerName); setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev); }; const handleError = (err) => { logError('โŒ Game error:', err); setError(err.message); }; // Approval system handlers (PRIVATE games only) const handlePendingApproval = (data) => { log('โณ Pending gamemaster approval:', data); setApprovalStatus('pending'); setError('Waiting for gamemaster approval...'); }; const handlePlayerRequestingJoin = (data) => { log('๐Ÿ”” Player requesting to join:', data.playerName); // Add to pending players list (for gamemaster) setPendingPlayers(prev => { if (prev.some(p => p.playerName === data.playerName)) return prev; return [...prev, { playerName: data.playerName, isAuthenticated: data.isAuthenticated, timestamp: data.timestamp }]; }); }; const handleApprovalGranted = (data) => { log('โœ… Join request approved:', data); setApprovalStatus('approved'); setError(null); // Player should now join the game rooms const socket = socketRef.current; if (socket && data.gameRoomName) { // Emit join-approved to notify backend socket.emit('game:join-approved', { gameToken }); } }; const handleApprovalDenied = (data) => { error('โŒ Join request denied:', data.reason || data.message); setApprovalStatus('denied'); setError(data.reason || 'Your request to join was denied'); }; const handlePlayerApproved = (data) => { log('โœ… Player approved by gamemaster:', data.playerName); // Remove from pending players list setPendingPlayers(prev => prev.filter(p => p.playerName !== data.playerName)); // Add to connected players setGameState(prev => { if (!prev) return prev; const currentConnected = prev.connectedPlayers || []; if (!currentConnected.includes(data.playerName)) { return { ...prev, connectedPlayers: [...currentConnected, data.playerName] }; } return prev; }); }; // Register all handlers socket.on('connect', handleConnect); socket.on('connect_error', handleConnectError); socket.on('disconnect', handleDisconnect); socket.on('game:state', handleGameState); socket.on('game:state-update', handleGameState); socket.on('game:joined', handleGameJoined); socket.on('game:player-joined', handlePlayerJoined); socket.on('game:started', handleGameStarted); socket.on('game:player-moved', handlePlayerMoved); socket.on('game:turn-changed', handleTurnChanged); socket.on('game:error', handleError); // Approval system events (PRIVATE games) socket.on('game:pending-approval', handlePendingApproval); socket.on('game:player-requesting-join', handlePlayerRequestingJoin); socket.on('game:approval-granted', handleApprovalGranted); socket.on('game:approval-denied', handleApprovalDenied); socket.on('game:player-approved', handlePlayerApproved); // Cleanup return () => { log('๐Ÿงน Cleaning up WebSocket connection'); socket.removeAllListeners(); socket.disconnect(); }; }, [gameToken]); // Optimized event listener management const addEventListener = useCallback((event, handler) => { const socket = socketRef.current; if (!socket) return; socket.on(event, handler); eventListenersRef.current.set(event, handler); }, []); const removeEventListener = useCallback((event) => { const socket = socketRef.current; if (!socket) return; const handler = eventListenersRef.current.get(event); if (handler) { socket.off(event, handler); eventListenersRef.current.delete(event); } }, []); // Memoized action methods - stable references const rollDice = useCallback((diceValue) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot roll dice: not connected'); return false; } log('๐ŸŽฒ Rolling dice:', diceValue); socket.emit('game:dice-roll', { gameCode: gameState?.gameCode, diceValue, }); return true; }, [isConnected, gameState?.gameCode]); const sendMessage = useCallback((message) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot send message: not connected'); return false; } socket.emit('game:chat', { gameCode: gameState?.gameCode, message, }); return true; }, [isConnected, gameState?.gameCode]); const setReady = useCallback((ready = true) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot set ready: not connected'); return false; } socket.emit('game:ready', { gameCode: gameState?.gameCode, ready, }); return true; }, [isConnected, gameState?.gameCode]); const leaveGame = useCallback(() => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot leave game: not connected'); return false; } socket.emit('game:leave', { gameCode: gameState?.gameCode, }); return true; }, [isConnected, gameState?.gameCode]); // Joker approval methods const approveJoker = useCallback((playerId, cardId, requestId) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot approve joker: not connected'); return false; } log('โœ… Approving joker for player:', playerId); socket.emit('game:gamemaster-decision', { gameCode: gameState?.gameCode, requestId: requestId || `joker-${playerId}-${cardId}`, decision: 'approve', }); return true; }, [isConnected, gameState?.gameCode]); const rejectJoker = useCallback((playerId, cardId, requestId) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot reject joker: not connected'); return false; } log('โŒ Rejecting joker for player:', playerId); socket.emit('game:gamemaster-decision', { gameCode: gameState?.gameCode, requestId: requestId || `joker-${playerId}-${cardId}`, decision: 'reject', }); return true; }, [isConnected, gameState?.gameCode]); // Card answer submission const submitAnswer = useCallback((cardId, answer) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot submit answer: not connected'); return false; } log('๐Ÿ“ Submitting answer:', answer); socket.emit('game:card-answer', { gameCode: gameState?.gameCode, cardId, answer, }); return true; }, [isConnected, gameState?.gameCode]); // Position guess submission const submitPositionGuess = useCallback((guessedPosition) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot submit position guess: not connected'); return false; } log('๐ŸŽฏ Submitting position guess:', guessedPosition); socket.emit('game:position-guess', { gameCode: gameState?.gameCode, guessedPosition, }); return true; }, [isConnected, gameState?.gameCode]); // Approve player (gamemaster only, PRIVATE games) const approvePlayer = useCallback((playerName) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot approve player: not connected'); return false; } if (!isGamemaster) { warn('โš ๏ธ Only gamemaster can approve players'); return false; } log('โœ… Approving player:', playerName); socket.emit('game:approve-player', { gameCode: gameState?.gameCode, playerName, }); return true; }, [isConnected, isGamemaster, gameState?.gameCode]); // Reject player (gamemaster only, PRIVATE games) const rejectPlayer = useCallback((playerName, reason = 'Join request denied') => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot reject player: not connected'); return false; } if (!isGamemaster) { warn('โš ๏ธ Only gamemaster can reject players'); return false; } log('โŒ Rejecting player:', playerName); socket.emit('game:reject-player', { gameCode: gameState?.gameCode, playerName, reason, }); return true; }, [isConnected, isGamemaster, gameState?.gameCode]); return { socket: socketRef.current, isConnected, gameState, players, boardData, currentTurn, error, isGamemaster, gameStarted, pendingPlayers, approvalStatus, // Methods rollDice, sendMessage, setReady, leaveGame, approveJoker, rejectJoker, submitAnswer, submitPositionGuess, approvePlayer, rejectPlayer, addEventListener, removeEventListener, }; };