import React, { createContext, useContext, useRef, useState, useCallback, useMemo, useEffect } from 'react'; import { io } from 'socket.io-client'; import { API_CONFIG } from '../api/userApi'; const GameWebSocketContext = createContext(null); 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); /** * Provider that maintains WebSocket connection across page navigation */ export const GameWebSocketProvider = ({ children }) => { const socketRef = useRef(null); const gameTokenRef = useRef(null); const [isConnected, setIsConnected] = useState(false); // Single game object containing all game data const [gameState, setGameState] = useState(null); // Structure: { // gameCode: string, // boardData: object, // turnInfo: { currentPlayer, currentPlayerName, turnNumber }, // players: [{ playerId, playerName, isOnline, isReady }], // playerPositions: { playerName: position }, // connectedPlayers: [], // readyPlayers: [], // state: string, // isGamemaster: boolean // } const [error, setError] = useState(null); const [isGamemaster, setIsGamemaster] = useState(false); const [gameStarted, setGameStarted] = useState(false); const [pendingPlayers, setPendingPlayers] = useState([]); const [approvalStatus, setApprovalStatus] = useState(null); const [playerIdentifier, setPlayerIdentifier] = useState(null); const [isMyTurnFlag, setIsMyTurnFlag] = useState(false); // Directly controlled by game:your-turn const [playerDiceRolls, setPlayerDiceRolls] = useState({}); // Memoized derived values - extract from single game object const players = useMemo(() => { return gameState?.players || []; }, [gameState?.players]); const playerPositions = useMemo(() => { return gameState?.playerPositions || {}; }, [gameState?.playerPositions]); const boardData = useMemo(() => { return gameState?.boardData || null; }, [gameState?.boardData]); const currentTurn = useMemo(() => gameState?.turnInfo?.currentPlayer || null, [gameState?.turnInfo?.currentPlayer]); const currentTurnName = useMemo(() => gameState?.turnInfo?.currentPlayerName || null, [gameState?.turnInfo?.currentPlayerName]); // isMyTurn is simply the flag set by game:your-turn event const isMyTurn = isMyTurnFlag; /** * Connect to game WebSocket with a game token * This maintains the connection even when navigating between pages */ const connect = useCallback((gameToken) => { if (!gameToken) { warn('โš ๏ธ Cannot connect without game token'); return; } // If already connected with same token, don't reconnect if (socketRef.current?.connected && gameTokenRef.current === gameToken) { log('โœ… Already connected with same token'); return; } // Disconnect old socket if exists if (socketRef.current) { log('๐Ÿ”Œ Disconnecting old socket'); socketRef.current.removeAllListeners(); socketRef.current.disconnect(); } log('๐Ÿ”Œ Connecting to game WebSocket...'); gameTokenRef.current = gameToken; // Decode token to get player identifier try { const payload = JSON.parse(atob(gameToken.split('.')[1])); const identifier = payload.userId || payload.playerName; setPlayerIdentifier(identifier); log('๐ŸŽฎ Player identifier:', identifier); } catch (err) { logError('Failed to decode game token:', err); } // 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 socket.on('connect', () => { log('โœ… Connected to game WebSocket'); setIsConnected(true); setError(null); socket.emit('game:join', { gameToken }); }); socket.on('connect_error', (err) => { logError('โŒ Connection error:', err); setIsConnected(false); setError(err.message); // If reconnection fails completely, navigate to home if (err.message?.includes('timeout') || err.message?.includes('xhr poll error')) { setTimeout(() => { if (!socketRef.current?.connected) { logError('โš ๏ธ Connection failed - redirecting to home'); window.location.href = '/'; } }, 10000); // Give 10 seconds for reconnection attempts } }); socket.on('disconnect', (reason) => { log('๐Ÿ”Œ Disconnected:', reason); setIsConnected(false); setIsMyTurnFlag(false); // Clear turn flag on disconnect }); // Game state handlers socket.on('game:state', (state) => { log('๐Ÿ“Š Game state received:', state); if (state?.isGamemaster !== undefined) { setIsGamemaster(state.isGamemaster); } // Merge state into single game object setGameState(prev => { // Build players array from various sources let playersArray = prev?.players || []; // If we have currentPlayers or players from backend, use them (already have proper structure) if (state.currentPlayers && Array.isArray(state.currentPlayers) && state.currentPlayers.length > 0) { playersArray = state.currentPlayers.map(p => ({ playerId: p.playerId || p.id, playerName: p.playerName || p.name, name: p.playerName || p.name, // Add name for compatibility isOnline: p.isOnline !== undefined ? p.isOnline : true, isReady: p.isReady || false })); } else if (state.players && Array.isArray(state.players) && state.players.length > 0) { playersArray = state.players.map(p => ({ playerId: p.playerId || p.id, playerName: p.playerName || p.name, name: p.playerName || p.name, // Add name for compatibility isOnline: p.isOnline !== undefined ? p.isOnline : true, isReady: p.isReady || false })); } // If players array is still empty but we have connectedPlayers (array of strings), convert them else if (playersArray.length === 0 && state.connectedPlayers && Array.isArray(state.connectedPlayers) && state.connectedPlayers.length > 0) { playersArray = state.connectedPlayers.map((playerName, index) => ({ playerId: `player-${index}`, // Temporary ID until we get the real one playerName: playerName, name: playerName, // Add name for compatibility isOnline: true, isReady: false })); } // Initialize playerPositions from backend data if joining game in progress let playerPositions = prev?.playerPositions || {}; // If we don't have positions yet but backend sent players with positions, initialize from them if (Object.keys(playerPositions).length === 0 && state.players && Array.isArray(state.players)) { const positionsFromBackend = {}; state.players.forEach(p => { const playerName = p.playerName || p.name; if (playerName && p.position !== undefined) { positionsFromBackend[playerName] = p.position; } }); if (Object.keys(positionsFromBackend).length > 0) { playerPositions = positionsFromBackend; } } const merged = { ...prev, gameCode: state.gameCode || prev?.gameCode, boardData: state.boardData || prev?.boardData, state: state.state || prev?.state, connectedPlayers: state.connectedPlayers || prev?.connectedPlayers || [], readyPlayers: state.readyPlayers || prev?.readyPlayers || [], // Preserve turn info - only turn-changed updates it turnInfo: prev?.turnInfo || { currentPlayer: null, currentPlayerName: null, turnNumber: 0 }, // Use the built players array players: playersArray, // Initialize playerPositions from backend if joining in progress, otherwise preserve playerPositions: playerPositions }; return merged; }); }); socket.on('game:state-update', (state) => { log('๐Ÿ“Š Game state update:', state); if (state?.isGamemaster !== undefined) { setIsGamemaster(state.isGamemaster); } // Merge state update into single game object setGameState(prev => ({ ...prev, gameCode: state.gameCode || prev?.gameCode, boardData: state.boardData || prev?.boardData, state: state.state || prev?.state, connectedPlayers: state.connectedPlayers || prev?.connectedPlayers, readyPlayers: state.readyPlayers || prev?.readyPlayers, // Preserve turn info and positions turnInfo: prev?.turnInfo, players: prev?.players, playerPositions: prev?.playerPositions })); }); socket.on('game:joined', (data) => { log('โœ… Joined game:', data); if (data.isGamemaster !== undefined) { setIsGamemaster(data.isGamemaster); } // Initialize or update gameState with gameCode and gameId from join confirmation setGameState(prev => { if (prev && prev.gameCode) { // If already has gameCode, just add gameId if missing return { ...prev, gameId: data.gameId || prev.gameId, gameCode: data.gameCode, playerName: data.playerName, isAuthenticated: data.isAuthenticated }; } return { ...prev, gameId: data.gameId, // Store gameId from backend gameCode: data.gameCode, playerName: data.playerName, isAuthenticated: data.isAuthenticated }; }); }); socket.on('game:player-joined', (data) => { log('๐Ÿ‘ค Player joined:', data.playerName); setGameState(prev => { if (!prev) return prev; const currentConnected = prev.connectedPlayers || []; const currentPlayers = prev.players || []; // Add to connectedPlayers if not already there const updatedConnected = currentConnected.includes(data.playerName) ? currentConnected : [...currentConnected, data.playerName]; // Add to players array if not already there (without position - that's in playerPositions) const playerExists = currentPlayers.some(p => p.playerName === data.playerName || p.name === data.playerName ); const updatedPlayers = playerExists ? currentPlayers : [...currentPlayers, { playerId: data.playerId, playerName: data.playerName, name: data.playerName, // Add name for compatibility with Lobby display isOnline: true, isReady: false }]; return { ...prev, connectedPlayers: updatedConnected, players: updatedPlayers }; }); window.dispatchEvent(new CustomEvent('game:player-joined', { detail: data })); }); socket.on('game:player-left', (data) => { log('๐Ÿ‘‹ Player left:', data.playerName); setGameState(prev => { if (!prev) return prev; return { ...prev, connectedPlayers: (prev.connectedPlayers || []).filter(p => p !== data.playerName) }; }); window.dispatchEvent(new CustomEvent('game:player-left', { detail: data })); }); socket.on('game:player-ready', (data) => { log('โœ… Player ready:', data.playerName); setGameState(prev => { if (!prev) return prev; const readyPlayers = prev.readyPlayers || []; if (data.ready && !readyPlayers.includes(data.playerName)) { return { ...prev, readyPlayers: [...readyPlayers, data.playerName] }; } else if (!data.ready) { return { ...prev, readyPlayers: readyPlayers.filter(p => p !== data.playerName) }; } return prev; }); window.dispatchEvent(new CustomEvent('game:player-ready', { detail: data })); }); socket.on('game:all-ready', (data) => { log('โœ… All players ready! Game can start.'); window.dispatchEvent(new CustomEvent('game:all-ready', { detail: data })); }); socket.on('game:start', (data) => { log('๐ŸŽฎ Game started:', data); setGameStarted(true); // Update game state with position initialization setGameState(prev => { // Initialize all player positions to 0 at game start const initialPositions = {}; // Use existing players from prev (already set by game:joined/game:state) const existingPlayers = prev?.players || []; // Initialize positions for all existing players existingPlayers.forEach((player) => { const playerName = player.playerName || player.name; if (playerName) { // Initialize ALL positions to 0 - only game:player-arrived will change them initialPositions[playerName] = 0; } }); const updated = { ...prev, gameCode: data.gameCode || prev?.gameCode, boardData: data.boardData || prev?.boardData, state: data.gamePhase || data.state || 'playing', boardSize: data.boardSize || prev?.boardSize, // Keep existing players list, initialize positions to 0 players: existingPlayers, playerPositions: initialPositions, // Preserve turn info and other data turnInfo: prev?.turnInfo || { currentPlayer: null, currentPlayerName: null, turnNumber: 0 }, connectedPlayers: prev?.connectedPlayers || [], readyPlayers: prev?.readyPlayers || [] }; return updated; }); }); socket.on('game:turn-changed', (data) => { log('๐Ÿ”„ Turn changed to:', data.currentPlayerName); // Turn changed means it's NOT my turn anymore setIsMyTurnFlag(false); // This is the ONLY place turnInfo should be set setGameState(prev => ({ ...prev, turnInfo: { currentPlayer: data.currentPlayer, currentPlayerName: data.currentPlayerName, turnNumber: data.turnNumber || prev?.turnInfo?.turnNumber || 0 } })); // Force a re-render by logging after state update setTimeout(() => { console.log('๐Ÿ”„ [game:turn-changed] State should be updated now'); }, 100); }); socket.on('game:error', (err) => { logError('โŒ Game error:', err); setError(err.message); }); // Approval system handlers socket.on('game:pending-approval', (data) => { log('โณ Pending gamemaster approval:', data); setApprovalStatus('pending'); setError('Waiting for gamemaster approval...'); }); socket.on('game:player-requesting-join', (data) => { log('๐Ÿ”” Player requesting to join:', data.playerName); setPendingPlayers(prev => { if (prev.some(p => p.playerName === data.playerName)) return prev; return [...prev, { playerName: data.playerName, isAuthenticated: data.isAuthenticated, timestamp: data.timestamp }]; }); }); socket.on('game:approval-granted', (data) => { log('โœ… Join request approved:', data); setApprovalStatus('approved'); setError(null); socket.emit('game:join-approved', { gameToken }); }); socket.on('game:approval-denied', (data) => { logError('โŒ Join request denied:', data.reason || data.message); setApprovalStatus('denied'); setError(data.reason || 'Your request to join was denied'); }); socket.on('game:player-approved', (data) => { log('โœ… Player approved by gamemaster:', data.playerName); setPendingPlayers(prev => prev.filter(p => p.playerName !== data.playerName)); setGameState(prev => { if (!prev) return prev; const currentConnected = prev.connectedPlayers || []; if (!currentConnected.includes(data.playerName)) { return { ...prev, connectedPlayers: [...currentConnected, data.playerName] }; } return prev; }); }); socket.on('game:your-turn', (data) => { log('๐ŸŽฏ Your turn!', data); console.log('๐ŸŽฏ [game:your-turn] Received - enabling dice'); // Set flag to true - dice is now enabled setIsMyTurnFlag(true); // Emit custom event for GameScreen to display turn notification window.dispatchEvent(new CustomEvent('game:your-turn', { detail: data })); }); socket.on('game:dice-rolled', (data) => { log('๐ŸŽฒ Dice rolled:', data.diceValue, 'by', data.playerName); // Track the dice roll for this player setPlayerDiceRolls(prev => ({ ...prev, [data.playerName]: data.diceValue })); window.dispatchEvent(new CustomEvent('game:dice-rolled', { detail: data })); }); socket.on('game:player-moving', (data) => { log('๐Ÿšถ Player moving:', data.playerName, 'from', data.fromPosition, 'to', data.toPosition); window.dispatchEvent(new CustomEvent('game:player-moving', { detail: data })); }); socket.on('game:player-arrived', (data) => { log('๐ŸŽฏ Player arrived:', data.playerName, 'at position', data.position, '(' + data.fieldType + ')'); // Update playerPositions in game object - THIS IS THE ONLY PLACE POSITIONS ARE MODIFIED setGameState(prev => { if (!prev) return prev; const updatedPositions = { ...prev.playerPositions, [data.playerName]: data.position }; return { ...prev, playerPositions: updatedPositions }; }); window.dispatchEvent(new CustomEvent('game:player-arrived', { detail: data })); }); socket.on('game:card-drawn', (data) => { log('๐Ÿƒ Card drawn:', data.cardType, 'by', data.playerName); window.dispatchEvent(new CustomEvent('game:card-drawn', { detail: data })); }); socket.on('game:card-result', (data) => { log('๐ŸŽด Card result:', data.playerName, data.description); window.dispatchEvent(new CustomEvent('game:card-result', { detail: data })); }); socket.on('game:answer-timeout', (data) => { log('โฐ Answer timeout:', data.playerName); window.dispatchEvent(new CustomEvent('game:answer-timeout', { detail: data })); }); socket.on('game:answer-result', (data) => { log('๐Ÿ“ Answer result:', data.correct ? 'โœ… Correct' : 'โŒ Wrong', '-', data.playerName); window.dispatchEvent(new CustomEvent('game:answer-result', { detail: data })); }); socket.on('game:gamemaster-decision-request', (data) => { log('๐Ÿ‘จโ€โš–๏ธ Gamemaster decision request:', data.playerName); window.dispatchEvent(new CustomEvent('game:gamemaster-decision-request', { detail: data })); }); socket.on('game:gamemaster-decision-result', (data) => { log('โš–๏ธ Gamemaster decision result:', data.decision, '-', data.playerName); window.dispatchEvent(new CustomEvent('game:gamemaster-decision-result', { detail: data })); }); socket.on('game:card-drawn-self', (data) => { log('๐Ÿƒ You drew a card:', data.cardType); window.dispatchEvent(new CustomEvent('game:card-drawn-self', { detail: data })); }); socket.on('game:joker-activated', (data) => { log('๐Ÿƒ Joker activated:', data.playerName); window.dispatchEvent(new CustomEvent('game:joker-activated', { detail: data })); }); socket.on('game:extra-turn', (data) => { log('โญ Extra turn:', data.playerName); window.dispatchEvent(new CustomEvent('game:extra-turn', { detail: data })); }); socket.on('game:turn-lost', (data) => { log('๐Ÿ˜ž Turn lost:', data.playerName); window.dispatchEvent(new CustomEvent('game:turn-lost', { detail: data })); }); socket.on('game:no-movement', (data) => { log('โ›” No movement:', data.playerName, '-', data.reason); window.dispatchEvent(new CustomEvent('game:no-movement', { detail: data })); }); socket.on('game:penalty-avoided', (data) => { log('๐Ÿ›ก๏ธ Penalty avoided:', data.playerName); window.dispatchEvent(new CustomEvent('game:penalty-avoided', { detail: data })); }); socket.on('game:guess-timeout', (data) => { log('โฐ Guess timeout:', data.playerName); window.dispatchEvent(new CustomEvent('game:guess-timeout', { detail: data })); }); socket.on('game:player-guessing', (data) => { log('๐Ÿค” Player guessing:', data.playerName); window.dispatchEvent(new CustomEvent('game:player-guessing', { detail: data })); }); socket.on('game:secondary-landing', (data) => { log('๐ŸŽฏ Secondary landing:', data.playerName, 'on', data.fieldType); window.dispatchEvent(new CustomEvent('game:secondary-landing', { detail: data })); }); socket.on('game:player-disconnected', (data) => { log('โš ๏ธ Player disconnected:', data.playerName); setGameState(prev => { if (!prev) return prev; return { ...prev, connectedPlayers: (prev.connectedPlayers || []).filter(p => p !== data.playerName) }; }); window.dispatchEvent(new CustomEvent('game:player-disconnected', { detail: data })); }); socket.on('game:player-disconnected-during-turn', (data) => { log('โš ๏ธ Player disconnected during turn:', data.playerName); window.dispatchEvent(new CustomEvent('game:player-disconnected-during-turn', { detail: data })); }); socket.on('game:player-disconnected-during-card', (data) => { log('โš ๏ธ Player disconnected during card:', data.playerName); window.dispatchEvent(new CustomEvent('game:player-disconnected-during-card', { detail: data })); }); socket.on('game:guess-result', (data) => { log('๐ŸŽฏ Guess result:', data); // Position update will come from game:player-arrived event window.dispatchEvent(new CustomEvent('game:guess-result', { detail: data })); }); socket.on('game:joker-complete', (data) => { log('๐Ÿƒ Joker complete:', data); // Position update will come from game:player-arrived event window.dispatchEvent(new CustomEvent('game:joker-complete', { detail: data })); }); socket.on('game:luck-consequence', (data) => { log('๐Ÿ€ Luck consequence:', data); // Position update will come from game:player-arrived event window.dispatchEvent(new CustomEvent('game:luck-consequence', { detail: data })); }); socket.on('game:ended', (data) => { log('๐Ÿ Game ended! Winner:', data.winner); setGameState(prev => ({ ...prev, status: 'finished', winner: data.winner, finalScores: data.scores })); window.dispatchEvent(new CustomEvent('game:ended', { detail: data })); // Don't auto-navigate - let the player close the winner modal manually // They can use the "Vissza a fล‘oldalra" button when ready }); socket.on('game:extra-turn-remaining', (data) => { log('โญ Extra turn remaining:', data); window.dispatchEvent(new CustomEvent('game:extra-turn-remaining', { detail: data })); }); socket.on('game:players-skipped', (data) => { log('โญ๏ธ Players skipped:', data.skippedPlayers); window.dispatchEvent(new CustomEvent('game:players-skipped', { detail: data })); }); socket.on('game:cleanup-complete', (data) => { log('๐Ÿงน Cleanup complete:', data); window.dispatchEvent(new CustomEvent('game:cleanup-complete', { detail: data })); // Navigate to home after cleanup setTimeout(() => { log('๐Ÿ  Navigating to home after cleanup'); window.location.href = '/'; }, 2000); }); }, []); /** * Disconnect from WebSocket */ const disconnect = useCallback(() => { if (socketRef.current) { log('๐Ÿ”Œ Disconnecting from game WebSocket'); socketRef.current.removeAllListeners(); socketRef.current.disconnect(); socketRef.current = null; gameTokenRef.current = null; setIsConnected(false); setGameState(null); // Clear entire game object setError(null); setIsGamemaster(false); setGameStarted(false); setPendingPlayers([]); setApprovalStatus(null); } }, []); // Action methods 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) 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) 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) return false; socket.emit('game:leave', { gameCode: gameState?.gameCode }); return true; }, [isConnected, gameState?.gameCode]); const approvePlayer = useCallback((playerName) => { const socket = socketRef.current; if (!socket || !isConnected || !isGamemaster) return false; socket.emit('game:approve-player', { gameCode: gameState?.gameCode, playerName }); return true; }, [isConnected, isGamemaster, gameState?.gameCode]); const rejectPlayer = useCallback((playerName, reason = 'Join request denied') => { const socket = socketRef.current; if (!socket || !isConnected || !isGamemaster) return false; socket.emit('game:reject-player', { gameCode: gameState?.gameCode, playerName, reason }); return true; }, [isConnected, isGamemaster, gameState?.gameCode]); const submitAnswer = useCallback((answer, cardId = null) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot submit answer: not connected'); return false; } log('๐Ÿ“ Submitting answer:', answer, 'for card:', cardId); socket.emit('game:card-answer', { gameCode: gameState?.gameCode, answer, cardId }); return true; }, [isConnected, gameState?.gameCode]); 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]); const submitJokerPositionGuess = useCallback((guessedPosition) => { const socket = socketRef.current; if (!socket || !isConnected) { warn('โš ๏ธ Cannot submit joker position guess: not connected'); return false; } log('๐Ÿƒ๐ŸŽฏ Submitting joker position guess:', guessedPosition); socket.emit('game:joker-position-guess', { gameCode: gameState?.gameCode, guessedPosition }); return true; }, [isConnected, gameState?.gameCode]); const approveJoker = useCallback((requestId) => { const socket = socketRef.current; if (!socket || !isConnected || !isGamemaster) { warn('โš ๏ธ Cannot approve joker: not gamemaster or not connected'); return false; } log('โœ… Approving joker request:', requestId); socket.emit('game:gamemaster-decision', { gameCode: gameState?.gameCode, requestId, decision: 'approve' }); return true; }, [isConnected, isGamemaster, gameState?.gameCode]); const rejectJoker = useCallback((requestId, reason = 'Joker answer rejected') => { const socket = socketRef.current; if (!socket || !isConnected || !isGamemaster) { warn('โš ๏ธ Cannot reject joker: not gamemaster or not connected'); return false; } log('โŒ Rejecting joker request:', requestId, 'Reason:', reason); socket.emit('game:gamemaster-decision', { gameCode: gameState?.gameCode, requestId, decision: 'reject', reason }); return true; }, [isConnected, isGamemaster, gameState?.gameCode]); const addEventListener = useCallback((event, handler) => { const socket = socketRef.current; if (socket) { socket.on(event, handler); } }, []); const removeEventListener = useCallback((event, handler) => { const socket = socketRef.current; if (socket) socket.off(event, handler); }, []); const value = { socket: socketRef.current, isConnected, gameState, // Single game object with all data players, // Memoized from gameState.players playerPositions, // Memoized from gameState.playerPositions (SINGLE SOURCE OF TRUTH for positions) boardData, // Memoized from gameState.boardData currentTurn, // Memoized from gameState.turnInfo.currentPlayer currentTurnName, // Memoized from gameState.turnInfo.currentPlayerName isMyTurn, playerIdentifier, error, isGamemaster, gameStarted, pendingPlayers, approvalStatus, playerDiceRolls, // Connection management connect, disconnect, // Methods rollDice, sendMessage, setReady, leaveGame, approvePlayer, rejectPlayer, submitAnswer, submitPositionGuess, submitJokerPositionGuess, approveJoker, rejectJoker, addEventListener, removeEventListener, }; return ( {children} ); }; /** * Hook to access the shared WebSocket connection */ export const useGameWebSocketContext = () => { const context = useContext(GameWebSocketContext); if (!context) { throw new Error('useGameWebSocketContext must be used within GameWebSocketProvider'); } return context; };