contextProvider #101
@@ -14,6 +14,7 @@ import gameRouter from './routers/gameRouter';
|
|||||||
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
||||||
import { WebSocketService } from '../Application/Services/WebSocketService';
|
import { WebSocketService } from '../Application/Services/WebSocketService';
|
||||||
import { GameWebSocketService } from '../Application/Services/GameWebSocketService';
|
import { GameWebSocketService } from '../Application/Services/GameWebSocketService';
|
||||||
|
import { container } from '../Application/Services/DIContainer';
|
||||||
import { GameRepository } from '../Infrastructure/Repository/GameRepository';
|
import { GameRepository } from '../Infrastructure/Repository/GameRepository';
|
||||||
import { UserRepository } from '../Infrastructure/Repository/UserRepository';
|
import { UserRepository } from '../Infrastructure/Repository/UserRepository';
|
||||||
import { RedisService } from '../Application/Services/RedisService';
|
import { RedisService } from '../Application/Services/RedisService';
|
||||||
@@ -183,17 +184,9 @@ AppDataSource.initialize()
|
|||||||
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Game WebSocket service for /game namespace
|
// Initialize Game WebSocket service for /game namespace via DIContainer
|
||||||
const gameRepository = new GameRepository();
|
container.setSocketIO(webSocketService['io']);
|
||||||
const userRepository = new UserRepository();
|
gameWebSocketService = container.gameWebSocketService;
|
||||||
const redisService = RedisService.getInstance();
|
|
||||||
|
|
||||||
gameWebSocketService = new GameWebSocketService(
|
|
||||||
webSocketService['io'], // Access the io property directly
|
|
||||||
gameRepository,
|
|
||||||
userRepository,
|
|
||||||
redisService
|
|
||||||
);
|
|
||||||
logStartup('Game WebSocket service initialized for /game namespace');
|
logStartup('Game WebSocket service initialized for /game namespace');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -226,21 +226,43 @@ export class StartGamePlayCommandHandler {
|
|||||||
|
|
||||||
private async notifyGameStart(game: GameAggregate): Promise<void> {
|
private async notifyGameStart(game: GameAggregate): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Note: WebSocket notifications will be handled when WebSocket service is available
|
// Get board data from Redis
|
||||||
// For now, just log the game start
|
const redisKey = `game_board_${game.id}`;
|
||||||
logOther('Game start notifications prepared', {
|
const boardDataStr = await this.redisService.get(redisKey);
|
||||||
|
|
||||||
|
if (!boardDataStr) {
|
||||||
|
logError('Board data not found in Redis during game start notification', new Error('Missing board data'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardData: BoardData = JSON.parse(boardDataStr);
|
||||||
|
|
||||||
|
// Get turn sequence from Redis
|
||||||
|
const gamePlayData = await this.getGamePlayFromRedis(game.id);
|
||||||
|
if (!gamePlayData) {
|
||||||
|
logError('Game play data not found in Redis', new Error('Missing game play data'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get WebSocket service from DIContainer and broadcast game start
|
||||||
|
const gameWebSocketService = DIContainer.getInstance().gameWebSocketService;
|
||||||
|
await gameWebSocketService.broadcastGameStart(
|
||||||
|
game.gamecode,
|
||||||
|
boardData,
|
||||||
|
gamePlayData.turnSequence,
|
||||||
|
game
|
||||||
|
);
|
||||||
|
|
||||||
|
logOther('Game start notifications sent via WebSocket', {
|
||||||
gameId: game.id,
|
gameId: game.id,
|
||||||
gameCode: game.gamecode,
|
gameCode: game.gamecode,
|
||||||
playerCount: game.players.length,
|
playerCount: game.players.length,
|
||||||
websocketRoom: `game_${game.gamecode}`
|
websocketRoom: `game_${game.gamecode}`,
|
||||||
|
firstPlayer: gamePlayData.turnSequence[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Implement WebSocket notifications when service is properly integrated
|
|
||||||
// wsService.notifyGameStart(game.gamecode, game.players);
|
|
||||||
// wsService.broadcastGameStateUpdate(game.gamecode, gameStateData);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to prepare game start notifications', error instanceof Error ? error : new Error(String(error)));
|
logError('Failed to send game start notifications', error instanceof Error ? error : new Error(String(error)));
|
||||||
// Don't throw error here - notification failure shouldn't prevent game start
|
// Don't throw error here - notification failure shouldn't prevent game start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ import { RedisService } from './RedisService';
|
|||||||
import { GameService } from '../Game/GameService';
|
import { GameService } from '../Game/GameService';
|
||||||
import { BoardGenerationService } from '../Game/BoardGenerationService';
|
import { BoardGenerationService } from '../Game/BoardGenerationService';
|
||||||
import { GenerateBoardCommandHandler } from '../Game/commands/GenerateBoardCommandHandler';
|
import { GenerateBoardCommandHandler } from '../Game/commands/GenerateBoardCommandHandler';
|
||||||
|
import { GameWebSocketService } from './GameWebSocketService';
|
||||||
|
import type { Server as SocketIOServer } from 'socket.io';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central Dependency Injection Container
|
* Central Dependency Injection Container
|
||||||
@@ -96,6 +98,8 @@ export class DIContainer {
|
|||||||
private _fieldEffectService: FieldEffectService | null = null;
|
private _fieldEffectService: FieldEffectService | null = null;
|
||||||
private _gameService: GameService | null = null;
|
private _gameService: GameService | null = null;
|
||||||
private _boardGenerationService: BoardGenerationService | null = null;
|
private _boardGenerationService: BoardGenerationService | null = null;
|
||||||
|
private _gameWebSocketService: GameWebSocketService | null = null;
|
||||||
|
private _socketIOInstance: SocketIOServer | null = null;
|
||||||
|
|
||||||
// Command Handlers
|
// Command Handlers
|
||||||
private _createUserCommandHandler: CreateUserCommandHandler | null = null;
|
private _createUserCommandHandler: CreateUserCommandHandler | null = null;
|
||||||
@@ -272,6 +276,30 @@ export class DIContainer {
|
|||||||
return this._boardGenerationService;
|
return this._boardGenerationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Socket.IO instance (must be called before accessing gameWebSocketService)
|
||||||
|
*/
|
||||||
|
public setSocketIO(io: SocketIOServer): void {
|
||||||
|
this._socketIOInstance = io;
|
||||||
|
// Reset gameWebSocketService so it gets recreated with new IO instance
|
||||||
|
this._gameWebSocketService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get gameWebSocketService(): GameWebSocketService {
|
||||||
|
if (!this._gameWebSocketService) {
|
||||||
|
if (!this._socketIOInstance) {
|
||||||
|
throw new Error('Socket.IO instance must be set before accessing gameWebSocketService. Call setSocketIO() first.');
|
||||||
|
}
|
||||||
|
this._gameWebSocketService = new GameWebSocketService(
|
||||||
|
this._socketIOInstance,
|
||||||
|
this.gameRepository as any, // Cast to concrete type
|
||||||
|
this.userRepository as any, // Cast to concrete type
|
||||||
|
RedisService.getInstance()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this._gameWebSocketService;
|
||||||
|
}
|
||||||
|
|
||||||
// Command Handler getters
|
// Command Handler getters
|
||||||
public get createUserCommandHandler(): CreateUserCommandHandler {
|
public get createUserCommandHandler(): CreateUserCommandHandler {
|
||||||
if (!this._createUserCommandHandler) {
|
if (!this._createUserCommandHandler) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import VerifyEmailPage from "./pages/Auth/VerifyEmailPage"
|
|||||||
import ChooseDeck from "./pages/Game/ChooseDeck"
|
import ChooseDeck from "./pages/Game/ChooseDeck"
|
||||||
import PlayerSetup from "./pages/Game/PlayerSetup"
|
import PlayerSetup from "./pages/Game/PlayerSetup"
|
||||||
import GameModalsDemo from "./pages/Game/GameModalsDemo"
|
import GameModalsDemo from "./pages/Game/GameModalsDemo"
|
||||||
|
import { GameWebSocketProvider } from "./contexts/GameWebSocketContext"
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
@@ -53,31 +54,33 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Router>
|
<GameWebSocketProvider>
|
||||||
<Routes>
|
<Router>
|
||||||
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmailPage />} />
|
<Routes>
|
||||||
<Route path={ROUTES.ABOUT} element={<About />} />
|
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmailPage />} />
|
||||||
<Route path={ROUTES.LOBBY} element={<Lobby />} />
|
<Route path={ROUTES.ABOUT} element={<About />} />
|
||||||
<Route path={ROUTES.REGISTER} element={<AuthRegister />} />
|
<Route path={ROUTES.LOBBY} element={<Lobby />} />
|
||||||
<Route path={ROUTES.LOGIN} element={<AuthLogin />} />
|
<Route path={ROUTES.REGISTER} element={<AuthRegister />} />
|
||||||
<Route path={ROUTES.FORGOT_PASSWORD} element={<ForgotPassword />} />
|
<Route path={ROUTES.LOGIN} element={<AuthLogin />} />
|
||||||
<Route path={ROUTES.RESET_PASSWORD} element={<ResetPassword />} />
|
<Route path={ROUTES.FORGOT_PASSWORD} element={<ForgotPassword />} />
|
||||||
<Route path={ROUTES.PROFILE} element={<ProfileCard />} />
|
<Route path={ROUTES.RESET_PASSWORD} element={<ResetPassword />} />
|
||||||
<Route path={ROUTES.TEST} element={<Test />} />
|
<Route path={ROUTES.PROFILE} element={<ProfileCard />} />
|
||||||
<Route path={ROUTES.ROOT} element={<Landingpage />} />
|
<Route path={ROUTES.TEST} element={<Test />} />
|
||||||
<Route path={ROUTES.HOME} element={<Home />} />
|
<Route path={ROUTES.ROOT} element={<Landingpage />} />
|
||||||
<Route path={ROUTES.DECKS} element={<DeckManagerPage />} />
|
<Route path={ROUTES.HOME} element={<Home />} />
|
||||||
<Route path={ROUTES.DECK_DETAILS} element={<Card_display />} />
|
<Route path={ROUTES.DECKS} element={<DeckManagerPage />} />
|
||||||
<Route path={ROUTES.DECK_CREATOR} element={<DeckCreator />} />
|
<Route path={ROUTES.DECK_DETAILS} element={<Card_display />} />
|
||||||
<Route path={ROUTES.DECK_CREATOR_EDIT} element={<DeckCreator />} />
|
<Route path={ROUTES.DECK_CREATOR} element={<DeckCreator />} />
|
||||||
<Route path={ROUTES.GAME} element={<GameScreen />} />
|
<Route path={ROUTES.DECK_CREATOR_EDIT} element={<DeckCreator />} />
|
||||||
<Route path={ROUTES.GAME_TEST} element={<GameTest />} />
|
<Route path={ROUTES.GAME} element={<GameScreen />} />
|
||||||
{/* <Route path={ROUTES.CONTACTS} element={<CompanyHub />} /> */}
|
<Route path={ROUTES.GAME_TEST} element={<GameTest />} />
|
||||||
<Route path={ROUTES.REPORTS} element={<Reports />} />
|
{/* <Route path={ROUTES.CONTACTS} element={<CompanyHub />} /> */}
|
||||||
<Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} />
|
<Route path={ROUTES.REPORTS} element={<Reports />} />
|
||||||
<Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} />
|
<Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} />
|
||||||
</Routes>
|
<Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} />
|
||||||
</Router>
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</GameWebSocketProvider>
|
||||||
|
|
||||||
{/* ✅ Toastify Container */}
|
{/* ✅ Toastify Container */}
|
||||||
<ToastConfig />
|
<ToastConfig />
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
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);
|
||||||
|
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([]);
|
||||||
|
const [approvalStatus, setApprovalStatus] = useState(null);
|
||||||
|
|
||||||
|
// Memoized derived values
|
||||||
|
const players = useMemo(() => {
|
||||||
|
const connectedPlayers = gameState?.connectedPlayers || [];
|
||||||
|
const currentPlayers = gameState?.currentPlayers || [];
|
||||||
|
|
||||||
|
if (currentPlayers.length > 0) {
|
||||||
|
return currentPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectedPlayers.length > 0) {
|
||||||
|
return connectedPlayers.map((nameOrObj, index) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
|
||||||
|
|
||||||
|
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
log('🔌 Disconnected:', reason);
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Game state handlers
|
||||||
|
socket.on('game:state', (state) => {
|
||||||
|
log('📊 Game state received:', state);
|
||||||
|
if (state?.isGamemaster !== undefined) {
|
||||||
|
setIsGamemaster(state.isGamemaster);
|
||||||
|
}
|
||||||
|
setGameState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game:state-update', (state) => {
|
||||||
|
log('📊 Game state update:', state);
|
||||||
|
if (state?.isGamemaster !== undefined) {
|
||||||
|
setIsGamemaster(state.isGamemaster);
|
||||||
|
}
|
||||||
|
setGameState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game:joined', (data) => {
|
||||||
|
log('✅ Joined game:', data);
|
||||||
|
if (data.isGamemaster !== undefined) {
|
||||||
|
setIsGamemaster(data.isGamemaster);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game:player-joined', (data) => {
|
||||||
|
log('👤 Player joined:', 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:start', (data) => {
|
||||||
|
log('🎮 Game started:', data);
|
||||||
|
setGameStarted(true);
|
||||||
|
|
||||||
|
// Store board data if provided
|
||||||
|
if (data.boardData) {
|
||||||
|
setBoardData(data.boardData);
|
||||||
|
log('✅ Board data stored from game:start event');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update game state with turn info
|
||||||
|
if (data.playerOrder) {
|
||||||
|
setGameState(prev => ({
|
||||||
|
...prev,
|
||||||
|
playerOrder: data.playerOrder,
|
||||||
|
currentPlayer: data.currentPlayer,
|
||||||
|
turnSequence: data.playerOrder
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game:started', (data) => {
|
||||||
|
log('🎮 Game started (legacy event):', data);
|
||||||
|
setGameStarted(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game:player-moved', (moveData) => {
|
||||||
|
log('🏃 Player moved:', moveData.playerName);
|
||||||
|
setGameState(prev => {
|
||||||
|
if (!prev?.currentPlayers) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
currentPlayers: prev.currentPlayers.map(p =>
|
||||||
|
p.playerId === moveData.playerId
|
||||||
|
? { ...p, boardPosition: moveData.newPosition }
|
||||||
|
: p
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('game:turn-changed', (data) => {
|
||||||
|
log('🔄 Turn changed to:', data.currentPlayerName);
|
||||||
|
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
setBoardData(null);
|
||||||
|
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 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,
|
||||||
|
players,
|
||||||
|
boardData,
|
||||||
|
currentTurn,
|
||||||
|
error,
|
||||||
|
isGamemaster,
|
||||||
|
gameStarted,
|
||||||
|
pendingPlayers,
|
||||||
|
approvalStatus,
|
||||||
|
// Connection management
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
// Methods
|
||||||
|
rollDice,
|
||||||
|
sendMessage,
|
||||||
|
setReady,
|
||||||
|
leaveGame,
|
||||||
|
approvePlayer,
|
||||||
|
rejectPlayer,
|
||||||
|
addEventListener,
|
||||||
|
removeEventListener,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameWebSocketContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</GameWebSocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||||
import { getVerticalOffset } from "../../utils/randomUtils"
|
import { getVerticalOffset } from "../../utils/randomUtils"
|
||||||
import Dice from "../../utils/dice/Dice"
|
import Dice from "../../utils/dice/Dice"
|
||||||
import { useGameWebSocket } from "../../hooks/useGameWebSocket"
|
import { useGameWebSocketContext } from "../../contexts/GameWebSocketContext"
|
||||||
import JokerApprovalModal from "./JokerApprovalModal"
|
import JokerApprovalModal from "./JokerApprovalModal"
|
||||||
import CardDisplayModal from "./CardDisplayModal"
|
import CardDisplayModal from "./CardDisplayModal"
|
||||||
import ConsequenceModal from "./ConsequenceModal"
|
import ConsequenceModal from "./ConsequenceModal"
|
||||||
@@ -45,8 +45,7 @@ const getDefaultFieldType = (count) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GameScreen = () => {
|
const GameScreen = () => {
|
||||||
// WebSocket connection
|
// WebSocket connection from context (maintains connection across navigation)
|
||||||
const gameToken = localStorage.getItem('gameToken')
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
gameState,
|
gameState,
|
||||||
@@ -61,7 +60,7 @@ const GameScreen = () => {
|
|||||||
submitPositionGuess,
|
submitPositionGuess,
|
||||||
addEventListener,
|
addEventListener,
|
||||||
removeEventListener
|
removeEventListener
|
||||||
} = useGameWebSocket(gameToken)
|
} = useGameWebSocketContext()
|
||||||
|
|
||||||
// Try to get boardData from WebSocket, fallback to localStorage
|
// Try to get boardData from WebSocket, fallback to localStorage
|
||||||
const boardData = useMemo(() => {
|
const boardData = useMemo(() => {
|
||||||
@@ -626,7 +625,7 @@ const GameScreen = () => {
|
|||||||
<div>👥 Players: {backendPlayers?.length || 0}</div>
|
<div>👥 Players: {backendPlayers?.length || 0}</div>
|
||||||
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
|
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
|
||||||
<div>🏁 Current Turn: {currentTurn || 'N/A'}</div>
|
<div>🏁 Current Turn: {currentTurn || 'N/A'}</div>
|
||||||
<div>🔑 Token: {gameToken ? '✅' : '❌'}</div>
|
{/* <div>🔑 Token: {gameToken ? '✅' : '❌'}</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate"
|
|||||||
import Navbar from "../../components/Navbar/Navbar.jsx"
|
import Navbar from "../../components/Navbar/Navbar.jsx"
|
||||||
import Background from "../../assets/backgrounds/Background.jsx"
|
import Background from "../../assets/backgrounds/Background.jsx"
|
||||||
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
|
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
|
||||||
import { useGameWebSocket } from "../../hooks/useGameWebSocket.js"
|
import { useGameWebSocketContext } from "../../contexts/GameWebSocketContext"
|
||||||
import { startGame } from "../../api/gameApi.js"
|
import { startGame } from "../../api/gameApi.js"
|
||||||
|
|
||||||
const Lobby = () => {
|
const Lobby = () => {
|
||||||
@@ -20,7 +20,9 @@ const Lobby = () => {
|
|||||||
const gameCodeFromState = location.state?.gameCode
|
const gameCodeFromState = location.state?.gameCode
|
||||||
const gameToken = localStorage.getItem('gameToken')
|
const gameToken = localStorage.getItem('gameToken')
|
||||||
|
|
||||||
|
// Use the shared WebSocket context
|
||||||
const {
|
const {
|
||||||
|
connect,
|
||||||
isConnected,
|
isConnected,
|
||||||
gameState,
|
gameState,
|
||||||
players,
|
players,
|
||||||
@@ -30,7 +32,14 @@ const Lobby = () => {
|
|||||||
approvalStatus,
|
approvalStatus,
|
||||||
approvePlayer,
|
approvePlayer,
|
||||||
rejectPlayer,
|
rejectPlayer,
|
||||||
} = useGameWebSocket(gameToken)
|
} = useGameWebSocketContext()
|
||||||
|
|
||||||
|
// Connect to WebSocket when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameToken) {
|
||||||
|
connect(gameToken)
|
||||||
|
}
|
||||||
|
}, [gameToken, connect])
|
||||||
|
|
||||||
const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...'
|
const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...'
|
||||||
|
|
||||||
@@ -67,18 +76,18 @@ const Lobby = () => {
|
|||||||
console.log('🎮 Game started, navigating to /game')
|
console.log('🎮 Game started, navigating to /game')
|
||||||
goGame()
|
goGame()
|
||||||
}
|
}
|
||||||
}, [gameStarted, navigate])
|
}, [gameStarted, goGame])
|
||||||
|
|
||||||
// Handle approval status changes
|
// Handle approval status changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (approvalStatus === 'denied') {
|
if (approvalStatus === 'denied') {
|
||||||
alert('A gamemaster elutasította a csatlakozási kérelmedet.')
|
alert('A gamemaster elutasította a csatlakozási kérelmedet.')
|
||||||
localStorage.removeItem('gameToken')
|
localStorage.removeItem('gameToken')
|
||||||
navigate("/home")
|
goHome()
|
||||||
} else if (approvalStatus === 'approved') {
|
} else if (approvalStatus === 'approved') {
|
||||||
console.log('✅ Join approved, you can now see the lobby')
|
console.log('✅ Join approved, you can now see the lobby')
|
||||||
}
|
}
|
||||||
}, [approvalStatus, navigate])
|
}, [approvalStatus, goHome])
|
||||||
|
|
||||||
const handleExit = () => {
|
const handleExit = () => {
|
||||||
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
|
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
|
||||||
@@ -116,7 +125,7 @@ const Lobby = () => {
|
|||||||
|
|
||||||
// Navigate immediately after successful start (don't wait for WebSocket)
|
// Navigate immediately after successful start (don't wait for WebSocket)
|
||||||
console.log('🎮 Navigating to /game...')
|
console.log('🎮 Navigating to /game...')
|
||||||
navigate('/game')
|
goGame()
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start game:', error)
|
console.error('Failed to start game:', error)
|
||||||
@@ -125,7 +134,7 @@ const Lobby = () => {
|
|||||||
if (error.response?.status === 409) {
|
if (error.response?.status === 409) {
|
||||||
console.log('Game already started, navigating to /game...')
|
console.log('Game already started, navigating to /game...')
|
||||||
// Navigate anyway - game is already running
|
// Navigate anyway - game is already running
|
||||||
navigate('/game')
|
goGame()
|
||||||
} else {
|
} else {
|
||||||
alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
|
alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user