feat: Pause/resume draft timers on host disconnect/reconnect and enable explicit player room departure.

This commit is contained in:
2025-12-16 21:37:37 +01:00
parent b9c5905474
commit 1c3758712d
5 changed files with 130 additions and 47 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -82,12 +82,18 @@ export const GameRoom: React.FC<GameRoomProps> = ({ 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);
};
}, []);

View File

@@ -182,12 +182,12 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ 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) {

View File

@@ -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<string, NodeJS.Timeout>();
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);
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,26 +153,46 @@ 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.`);
}
}
const room = roomManager.getRoom(roomId);
if (room) {
socket.emit('room_update', 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);
}
});
socket.on('leave_room', ({ roomId, playerId }) => {
const room = roomManager.leaveRoom(roomId, playerId);
socket.leave(roomId);
if (room) {
console.log(`Player ${playerId} left room ${roomId}`);
io.to(roomId).emit('room_update', room);
} else {
console.log(`Room ${roomId} closed/empty`);
}
});
socket.on('send_message', ({ roomId, sender, text }) => {
@@ -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.
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);
}
playerTimers.delete(playerId);
}, 30000); // 30 seconds
playerTimers.set(playerId, timer);
}
}
});