csatlakozas-mukodesdemodemodemo
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
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 error = (...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 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: full player objects with game data (positions, etc.)
|
||||
const connectedPlayers = gameState?.connectedPlayers || [];
|
||||
const gamePlayers = gameState?.players || [];
|
||||
const currentPlayers = gameState?.currentPlayers || [];
|
||||
|
||||
// If we have full player objects, use those
|
||||
if (currentPlayers.length > 0) return currentPlayers;
|
||||
if (gamePlayers.length > 0) return gamePlayers;
|
||||
|
||||
// Otherwise, map connected player names to basic player objects
|
||||
return connectedPlayers.map((name, index) => ({
|
||||
id: `player-${index}`,
|
||||
name: typeof name === 'string' ? name : name.playerName || `Player ${index + 1}`,
|
||||
isOnline: true,
|
||||
isReady: gameState?.readyPlayers?.includes(name) || false,
|
||||
}));
|
||||
}, [gameState?.connectedPlayers, gameState?.players, 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:', state);
|
||||
setGameState(state);
|
||||
};
|
||||
|
||||
const handleGameJoined = (data) => {
|
||||
log('✅ Joined game:', data);
|
||||
// Store if this user is the gamemaster
|
||||
if (data.isGamemaster !== undefined) {
|
||||
setIsGamemaster(data.isGamemaster);
|
||||
}
|
||||
// Backend will send game:state next
|
||||
};
|
||||
|
||||
const handlePlayerJoined = (data) => {
|
||||
log('👤 Player joined:', data.playerName);
|
||||
// Update game state to add the new player to connectedPlayers
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
|
||||
const currentConnected = prev.connectedPlayers || [];
|
||||
// Only add if not already in the list
|
||||
if (!currentConnected.includes(data.playerName)) {
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: [...currentConnected, data.playerName]
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleGameStarted = (data) => {
|
||||
log('🎮 Game started:', data);
|
||||
// Batch state updates
|
||||
if (data.boardData) setBoardData(data.boardData);
|
||||
if (data.gameState) setGameState(data.gameState);
|
||||
// Signal that game has started
|
||||
setGameStarted(true);
|
||||
};
|
||||
|
||||
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) => {
|
||||
error('❌ Game error:', err);
|
||||
setError(err.message);
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// 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]);
|
||||
|
||||
return {
|
||||
socket: socketRef.current,
|
||||
isConnected,
|
||||
gameState,
|
||||
players,
|
||||
boardData,
|
||||
currentTurn,
|
||||
error,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
// Methods
|
||||
rollDice,
|
||||
sendMessage,
|
||||
setReady,
|
||||
leaveGame,
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user