From 65824a52d91a0239fdd8512272f63f9990a1a666 Mon Sep 17 00:00:00 2001 From: dnviti Date: Sun, 14 Dec 2025 22:41:26 +0100 Subject: [PATCH] fix: Resolve React hooks violation, implement player waiting screen, and auto-start game upon deck submission. --- docs/development/CENTRAL.md | 2 + .../2025-12-14-233000_fix_submit_deck.md | 28 ++++ ...2-14-234500_fix_hooks_and_waiting_state.md | 22 ++++ .../src/modules/draft/DeckBuilderView.tsx | 2 +- src/client/src/modules/lobby/GameRoom.tsx | 122 ++++++++++++------ src/server/index.ts | 49 +++++-- src/server/managers/RoomManager.ts | 18 ++- 7 files changed, 190 insertions(+), 53 deletions(-) create mode 100644 docs/development/devlog/2025-12-14-233000_fix_submit_deck.md create mode 100644 docs/development/devlog/2025-12-14-234500_fix_hooks_and_waiting_state.md diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 791ea5f..bb11041 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -15,6 +15,8 @@ The project has successfully migrated from a .NET backend to a Node.js Modular M - **[2025-12-14] Draft & Deck Builder**: Implemented full draft simulation (Pick/Pass) and Deck Construction with land station. [Link](./devlog/2025-12-14-223000_draft_and_deckbuilder.md) - **[2025-12-14] Image Caching**: Implemented server-side image caching to ensure reliable card rendering. [Link](./devlog/2025-12-14-224500_image_caching.md) - **[2025-12-14] Fix Draft Images**: Fixed image loading in Draft UI by adding proxy configuration and correcting property access. [Link](./devlog/2025-12-14-230000_fix_draft_images.md) +- **[2025-12-14] Fix Submit Deck**: Implemented `player_ready` handler and state transition to auto-start game when deck is submitted. [Link](./devlog/2025-12-14-233000_fix_submit_deck.md) +- **[2025-12-14] Fix Hooks & Waiting State**: Resolved React hook violation crash and added proper waiting screen for ready players. [Link](./devlog/2025-12-14-234500_fix_hooks_and_waiting_state.md) ## Active Modules 1. **Cube Manager**: Fully functional (Parsing, Fetching, Pack Generation). diff --git a/docs/development/devlog/2025-12-14-233000_fix_submit_deck.md b/docs/development/devlog/2025-12-14-233000_fix_submit_deck.md new file mode 100644 index 0000000..88701c4 --- /dev/null +++ b/docs/development/devlog/2025-12-14-233000_fix_submit_deck.md @@ -0,0 +1,28 @@ +# Fix Submit Deck Button + +## Issue +Users reported that "Submit Deck" button was not working. + +## Root Causes +1. **Missing Event Handler**: The server was not listening for the `player_ready` event emitted by the client. +2. **Incomplete Payload**: The client was sending `{ roomId, deck }` but the server needed `playerId` to identify who was ready, which was missing from the payload. +3. **Missing State Logic**: The `RoomManager` did not have a concept of "Ready" state or "Playing" status, meaning the transition from Deck Building to Game was not fully implemented. + +## Fixes +1. **Client (`DeckBuilderView.tsx`)**: Updated `player_ready` emission to include `playerId`. +2. **Server (`RoomManager.ts`)**: + - Added `ready` and `deck` properties to `Player` interface. + - Added `playing` to `Room` status. + - Implemented `setPlayerReady` method. +3. **Server (`index.ts`)**: + - Implemented `player_ready` socket handler. + - Added logic to check if *all* active players are ready. + - If all ready, automatically transitions room status to `playing` and initializes the game using `GameManager`, loading the submitted decks. + - ensured deck loading uses cached images (`card.image`) if available. + +## Verification +1. Draft cards. +2. Build deck. +3. Click "Submit Deck". +4. Server logs should show "All players ready...". +5. Client should automatically switch to `GameView` (Battlefield). diff --git a/docs/development/devlog/2025-12-14-234500_fix_hooks_and_waiting_state.md b/docs/development/devlog/2025-12-14-234500_fix_hooks_and_waiting_state.md new file mode 100644 index 0000000..ee10070 --- /dev/null +++ b/docs/development/devlog/2025-12-14-234500_fix_hooks_and_waiting_state.md @@ -0,0 +1,22 @@ +# Fix Hooks Violation and Implement Waiting State + +## Issue +1. **React Hook Error**: Users encountered "Rendered fewer hooks than expected" when the game started. This was caused by conditional returns in `GameRoom.tsx` appearing *before* hook declarations (`useState`, `useEffect`). +2. **UX Issue**: Players who submitted their decks remained in the Deck Builder view, able to modify their decks, instead of seeing a waiting screen. + +## Fixes +1. **Refactored `GameRoom.tsx`**: + - Moved all `useState` and `useEffect` hooks to the top level of the component, ensuring they are always called regardless of the render logic. + - Encapsulated the view switching logic into a helper function `renderContent()`, which is called inside the main return statement. +2. **Implemented Waiting Screen**: + - Inside `renderContent`, checking if the room is in `deck_building` status AND if the current player has `ready: true`. + - If ready, displays a "Deck Submitted" screen with a list of other players and their readiness status. + - Updated the sidebar player list to show a "• Ready" indicator. + +## Verification +1. Start a draft with multiple users (or simulate it). +2. Complete draft and enter deck building. +3. Submit deck as one player. +4. Verify that the view changes to "Deck Submitted" / Waiting screen. +5. Submit deck as the final player. +6. Verify that the game starts automatically for everyone without crashing (React Error). diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index c6eb241..ceddedc 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -87,7 +87,7 @@ export const DeckBuilderView: React.FC = ({ roomId, curren // Actually, user rules say "Host ... guided ... configuring packs ... multiplayer". // I'll emit 'submit_deck' event (need to handle in server) - socketService.socket.emit('player_ready', { roomId, deck: fullDeck }); + socketService.socket.emit('player_ready', { roomId, playerId: currentPlayerId, deck: fullDeck }); }; return ( diff --git a/src/client/src/modules/lobby/GameRoom.tsx b/src/client/src/modules/lobby/GameRoom.tsx index fc4e424..110f0f2 100644 --- a/src/client/src/modules/lobby/GameRoom.tsx +++ b/src/client/src/modules/lobby/GameRoom.tsx @@ -76,6 +76,18 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); + // New States + const [draftState, setDraftState] = useState(null); + + useEffect(() => { + const socket = socketService.socket; + const handleDraftUpdate = (data: any) => { + setDraftState(data); + }; + socket.on('draft_update', handleDraftUpdate); + return () => { socket.off('draft_update', handleDraftUpdate); }; + }, []); + const sendMessage = (e: React.FormEvent) => { e.preventDefault(); if (!message.trim()) return; @@ -132,35 +144,54 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl socketService.socket.emit('start_draft', { roomId: room.id }); }; - if (gameState) { - return ; - } + // Helper to determine view + const renderContent = () => { + if (gameState) { + return ; + } - // New States - const [draftState, setDraftState] = useState(null); + if (room.status === 'drafting' && draftState) { + return ; + } - useEffect(() => { - const socket = socketService.socket; - const handleDraftUpdate = (data: any) => { - setDraftState(data); - }; - socket.on('draft_update', handleDraftUpdate); - return () => { socket.off('draft_update', handleDraftUpdate); }; - }, []); + if (room.status === 'deck_building' && draftState) { + // Check if I am ready + // Type casting needed because 'ready' was added to interface only in server side so far? + // Need to update client Player interface too in this file if not already consistent. + // But let's assume raw object has it. + const me = room.players.find(p => p.id === currentPlayerId) as any; + if (me?.ready) { + return ( +
+

Deck Submitted

+
+ +
+

Waiting for other players to finish deck building...

+
+

Players Ready

+
+ {room.players.filter(p => p.role === 'player').map(p => { + const isReady = (p as any).ready; + return ( +
+
+ {p.name} +
+ ); + })} +
+
+
+ ); + } - if (room.status === 'drafting' && draftState) { - return ; - } + const myPool = draftState.players[currentPlayerId]?.pool || []; + return ; + } - if (room.status === 'deck_building' && draftState) { - // Get my pool - const myPool = draftState.players[currentPlayerId]?.pool || []; - return ; - } - - return ( -
- {/* Main Game Area (Placeholder for now) */} + // Default Waiting Lobby + return (

Waiting for Players...

@@ -201,6 +232,12 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl
)}
+ ); + }; + + return ( +
+ {renderContent()} {/* Sidebar: Players & Chat */}
@@ -210,23 +247,28 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl Lobby
- {room.players.map(p => ( -
-
-
- {p.name.substring(0, 2).toUpperCase()} -
-
- - {p.name} - - - {p.role} {p.isHost && • Host} - + {room.players.map(p => { + // Cast to any to access ready state without full interface update for now + const isReady = (p as any).ready; + return ( +
+
+
+ {p.name.substring(0, 2).toUpperCase()} +
+
+ + {p.name} + + + {p.role} {p.isHost && • Host} + {isReady && room.status === 'deck_building' && • Ready} + +
-
- ))} + ) + })}
diff --git a/src/server/index.ts b/src/server/index.ts index cef7ed7..06b7bf1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -104,16 +104,6 @@ io.on('connection', (socket) => { } }); - socket.on('pick_card', ({ roomId, cardId }) => { - // Find player from socket? Actually we trust clientId sent or inferred (but simpler to trust socket for now if we tracked map, but here just use helper?) - // We didn't store socket->player map here globally. We'll pass playerId in payload for simplicity but validation later. - // Wait, let's look at signature.. pickCard(roomId, playerId, cardId) - - // Need playerId. Let's ask client to send it. - // Or we can find it if we know connection... - // Let's assume payload: { roomId, playerId, cardId } - }); - // Revised pick_card to actual impl socket.on('pick_card', ({ roomId, playerId, cardId }) => { const draft = draftManager.pickCard(roomId, playerId, cardId); @@ -131,6 +121,45 @@ io.on('connection', (socket) => { } }); + socket.on('player_ready', ({ roomId, playerId, deck }) => { + const room = roomManager.setPlayerReady(roomId, playerId, deck); + if (room) { + io.to(roomId).emit('room_update', room); + + // Check if all active players are ready + const activePlayers = room.players.filter(p => p.role === 'player'); + if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) { + console.log(`All players ready in room ${roomId}. Starting game...`); + + room.status = 'playing'; + io.to(roomId).emit('room_update', room); + + // Initialize Game + const game = gameManager.createGame(roomId, room.players); + + // Load decks + activePlayers.forEach(p => { + if (p.deck) { + p.deck.forEach((card: any) => { + gameManager.addCardToGame(roomId, { + ownerId: p.id, + controllerId: p.id, + oracleId: card.oracle_id || card.id, + name: card.name, + // Prioritize 'image' property which might hold the cached URL + imageUrl: card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal || "", + zone: 'library' + }); + }); + // TODO: Shuffle library + } + }); + + io.to(roomId).emit('game_update', game); + } + } + }); + socket.on('start_game', ({ roomId, decks }) => { const room = roomManager.startGame(roomId); if (room) { diff --git a/src/server/managers/RoomManager.ts b/src/server/managers/RoomManager.ts index 91e38eb..63aacc9 100644 --- a/src/server/managers/RoomManager.ts +++ b/src/server/managers/RoomManager.ts @@ -3,6 +3,8 @@ interface Player { name: string; isHost: boolean; role: 'player' | 'spectator'; + ready?: boolean; + deck?: any[]; } interface ChatMessage { @@ -17,7 +19,7 @@ interface Room { hostId: string; players: Player[]; packs: any[]; // Store generated packs (JSON) - status: 'waiting' | 'drafting' | 'deck_building' | 'finished'; + status: 'waiting' | 'drafting' | 'deck_building' | 'playing' | 'finished'; messages: ChatMessage[]; maxPlayers: number; } @@ -30,7 +32,7 @@ export class RoomManager { const room: Room = { id: roomId, hostId, - players: [{ id: hostId, name: hostName, isHost: true, role: 'player' }], + players: [{ id: hostId, name: hostName, isHost: true, role: 'player', ready: false }], packs, status: 'waiting', messages: [], @@ -40,6 +42,18 @@ export class RoomManager { return room; } + setPlayerReady(roomId: string, playerId: string, deck: any[]): Room | null { + const room = this.rooms.get(roomId); + if (!room) return null; + + const player = room.players.find(p => p.id === playerId); + if (player) { + player.ready = true; + player.deck = deck; + } + return room; + } + joinRoom(roomId: string, playerId: string, playerName: string): Room | null { const room = this.rooms.get(roomId); if (!room) return null;