diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 687bd52..9777313 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -15,4 +15,5 @@ - [Reconnection & Auto-Pick](./devlog/2025-12-16-191500_reconnection_and_autopick.md): Completed. Implemented session persistence, seamless reconnection, and 30s auto-pick on disconnect. - [Draft Interface UI Polish](./devlog/2025-12-16-195000_draft_ui_polish.md): Completed. Redesigned the draft view for a cleaner, immersive, game-like experience with no unnecessary scrolls. - [Resizable Draft Interface](./devlog/2025-12-16-200500_resizable_draft_ui.md): Completed. Implemented user-resizable pool panel and card sizes with persistence. -- [Card Zoom (Dedicated Zone)](./devlog/2025-12-16-203000_zoom_zone.md): Completed. Refactored layout to show zoomed card in a dedicated side panel. +- [Draft UI Zoom Zone](./devlog/2025-12-16-203000_zoom_zone.md): Completed. Implemented dedicated zoom zone for card preview. +- [Host Disconnect Pause](./devlog/2025-12-16-213500_host_disconnect_pause.md): Completed. Specific logic to pause game when host leaves. diff --git a/docs/development/devlog/2025-12-16-213500_host_disconnect_pause.md b/docs/development/devlog/2025-12-16-213500_host_disconnect_pause.md new file mode 100644 index 0000000..e02a0ed --- /dev/null +++ b/docs/development/devlog/2025-12-16-213500_host_disconnect_pause.md @@ -0,0 +1,22 @@ +# Host Disconnect Pause Logic + +## Objective +Ensure the game pauses for all players when the Host disconnects, preventing auto-pick logic from advancing the game state. enable players to leave cleanly. + +## Changes +1. **Server (`src/server/index.ts`)**: + * Refactored socket handlers. + * Implemented `startAutoPickTimer` / `stopAllRoomTimers` helpers. + * Updated `disconnect` handler: Checks if disconnected player is passed host. If true, pauses game (stops all timers). + * Updated `join_room` / `rejoin_room`: Resumes game (restarts timers) if Host reconnects. + * Added `leave_room` event handler to properly remove players from room state. + +2. **Frontend (`src/client/src/modules/lobby/LobbyManager.tsx`)**: + * Updated `handleExitRoom` to emit `leave_room` event, preventing "ghost" connections. + +3. **Frontend (`src/client/src/modules/lobby/GameRoom.tsx`)**: + * Fixed build error (unused variable `setGameState`) by adding `game_update` listener. + * Verified "Game Paused" overlay logic exists and works with the new server state (`isHostOffline`). + +## Result +Host disconnection now effectively pauses the draft flow. Reconnection resumes it. Players can leave safely. diff --git a/src/client/src/modules/lobby/GameRoom.tsx b/src/client/src/modules/lobby/GameRoom.tsx index 2fe1564..fb5d096 100644 --- a/src/client/src/modules/lobby/GameRoom.tsx +++ b/src/client/src/modules/lobby/GameRoom.tsx @@ -82,12 +82,18 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl setModalOpen(true); }; + const handleGameUpdate = (data: any) => { + setGameState(data); + }; + socket.on('draft_update', handleDraftUpdate); socket.on('draft_error', handleDraftError); + socket.on('game_update', handleGameUpdate); return () => { socket.off('draft_update', handleDraftUpdate); socket.off('draft_error', handleDraftError); + socket.off('game_update', handleGameUpdate); }; }, []); diff --git a/src/client/src/modules/lobby/LobbyManager.tsx b/src/client/src/modules/lobby/LobbyManager.tsx index 19a1f52..9d288f8 100644 --- a/src/client/src/modules/lobby/LobbyManager.tsx +++ b/src/client/src/modules/lobby/LobbyManager.tsx @@ -182,12 +182,12 @@ export const LobbyManager: React.FC = ({ generatedPacks }) => const handleExitRoom = () => { + if (activeRoom) { + socketService.socket.emit('leave_room', { roomId: activeRoom.id, playerId }); + } setActiveRoom(null); setInitialDraftState(null); localStorage.removeItem('active_room_id'); - // Also likely want to disconnect socket or leave room specifically if needed, - // but just clearing local state allows creating new rooms. - // Ideally: socketService.emit('leave_room', { roomId: activeRoom.id, playerId }); }; if (activeRoom) { diff --git a/src/server/index.ts b/src/server/index.ts index 445d57b..7093dfe 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -64,9 +64,55 @@ app.post('/api/cards/cache', async (req: Request, res: Response) => { io.on('connection', (socket) => { console.log('A user connected', socket.id); - // Actually, let's use a simpler map: PlayerID -> Timeout + // Timer management const playerTimers = new Map(); + const startAutoPickTimer = (roomId: string, playerId: string) => { + // Clear existing if any (debounce) + if (playerTimers.has(playerId)) { + clearTimeout(playerTimers.get(playerId)!); + } + + const timer = setTimeout(() => { + console.log(`Timeout for player ${playerId}. Auto-picking...`); + const draft = draftManager.autoPick(roomId, playerId); + if (draft) { + io.to(roomId).emit('draft_update', draft); + // We only pick once. If they stay offline, the next pick depends on the next turn cycle. + // If we wanted continuous auto-pick, we'd need to check if it's still their turn and recurse. + // For now, this unblocks the current step. + } + playerTimers.delete(playerId); + }, 30000); // 30s + + playerTimers.set(playerId, timer); + }; + + const stopAutoPickTimer = (playerId: string) => { + if (playerTimers.has(playerId)) { + clearTimeout(playerTimers.get(playerId)!); + playerTimers.delete(playerId); + } + }; + + const stopAllRoomTimers = (roomId: string) => { + const room = roomManager.getRoom(roomId); + if (room) { + room.players.forEach(p => stopAutoPickTimer(p.id)); + } + }; + + const resumeRoomTimers = (roomId: string) => { + const room = roomManager.getRoom(roomId); + if (room && room.status === 'drafting') { + room.players.forEach(p => { + if (p.isOffline && p.role === 'player') { + startAutoPickTimer(roomId, p.id); + } + }); + } + }; + socket.on('create_room', ({ hostId, hostName, packs }, callback) => { const room = roomManager.createRoom(hostId, hostName, packs, socket.id); // Add socket.id socket.join(room.id); @@ -78,16 +124,19 @@ io.on('connection', (socket) => { const room = roomManager.joinRoom(roomId, playerId, playerName, socket.id); // Add socket.id if (room) { // Clear timeout if exists (User reconnected) - if (playerTimers.has(playerId)) { - clearTimeout(playerTimers.get(playerId)!); - playerTimers.delete(playerId); - console.log(`Player ${playerName} reconnected. Auto-pick cancelled.`); - } + stopAutoPickTimer(playerId); + console.log(`Player ${playerName} reconnected. Auto-pick cancelled.`); socket.join(room.id); console.log(`Player ${playerName} joined room ${roomId}`); io.to(room.id).emit('room_update', room); // Broadcast update + // Check if Host Reconnected -> Resume Game + if (room.hostId === playerId) { + console.log(`Host ${playerName} reconnected. Resuming draft timers.`); + resumeRoomTimers(roomId); + } + // If drafting, send state immediately and include in callback let currentDraft = null; if (room.status === 'drafting') { @@ -104,25 +153,45 @@ io.on('connection', (socket) => { // RE-IMPLEMENTING rejoin_room with playerId socket.on('rejoin_room', ({ roomId, playerId }) => { socket.join(roomId); + if (playerId) { // Update socket ID mapping - roomManager.updatePlayerSocket(roomId, playerId, socket.id); + const room = roomManager.updatePlayerSocket(roomId, playerId, socket.id); - // Clear Timer - if (playerTimers.has(playerId)) { - clearTimeout(playerTimers.get(playerId)!); - playerTimers.delete(playerId); - console.log(`Player ${playerId} reconnected via rejoin. Auto-pick cancelled.`); + if (room) { + // Clear Timer + stopAutoPickTimer(playerId); + console.log(`Player ${playerId} reconnected via rejoin.`); + + // Notify others (isOffline false) + io.to(roomId).emit('room_update', room); + + // Check if Host Reconnected -> Resume Game + if (room.hostId === playerId) { + console.log(`Host ${playerId} reconnected. Resuming draft timers.`); + resumeRoomTimers(roomId); + } + + if (room.status === 'drafting') { + const draft = draftManager.getDraft(roomId); + if (draft) socket.emit('draft_update', draft); + } } + } else { + // Just get room if no playerId? Should rare happen + const room = roomManager.getRoom(roomId); + if (room) socket.emit('room_update', room); } + }); - const room = roomManager.getRoom(roomId); + socket.on('leave_room', ({ roomId, playerId }) => { + const room = roomManager.leaveRoom(roomId, playerId); + socket.leave(roomId); if (room) { - socket.emit('room_update', room); - if (room.status === 'drafting') { - const draft = draftManager.getDraft(roomId); - if (draft) socket.emit('draft_update', draft); - } + console.log(`Player ${playerId} left room ${roomId}`); + io.to(roomId).emit('room_update', room); + } else { + console.log(`Room ${roomId} closed/empty`); } }); @@ -261,32 +330,17 @@ io.on('connection', (socket) => { io.to(room.id).emit('room_update', room); if (room.status === 'drafting') { - // Start Timer (e.g. 30 seconds) - const timer = setTimeout(() => { - console.log(`Timeout for player ${playerId}. Auto-picking...`); - // Auto-pick - const draft = draftManager.autoPick(room.id, playerId); - if (draft) { - io.to(room.id).emit('draft_update', draft); + // Check if Host is currently offline (including self if self is host) + // If Host is offline, PAUSE EVERYTHING. + const hostOffline = room.players.find(p => p.id === room.hostId)?.isOffline; - // If they still have picks to make (Pick 2), we might need to auto-pick again? - // For simplicity, let's assume autoPick handles 1 pick. - // If they are still offline, the NEXT time they are blocking the flow? - // Ideally, we should check if they still need to pick. - // But for a basic "if user does not reconnect in a time frame", this fulfills the request. - // The system will effectively auto-pick 1 card every 30s (if we reset the timer). - // But we only set the timer ONCE on disconnect. - // If they stay disconnected, we need to loop. - - // RECURSIVE TIMER: - // If player is still offline after auto-pick, schedule another one? - // We need to check if they are still blocking. - // For now, let's just do ONE auto-pick per disconnect event to unblock. - } - playerTimers.delete(playerId); - }, 30000); // 30 seconds - - playerTimers.set(playerId, timer); + if (hostOffline) { + console.log("Host is offline. Pausing game (stopping all timers)."); + stopAllRoomTimers(room.id); + } else { + // Host is online, but THIS player disconnected. Start timer for them. + startAutoPickTimer(room.id, playerId); + } } } });