diff --git a/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts b/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts index 297f3cc9..760dd218 100644 --- a/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts +++ b/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts @@ -197,9 +197,8 @@ export class GameWebSocketService { private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise { try { - // Simple data extraction - let Socket.IO handle the parsing - const jsdata = JSON.parse(data); - const gameToken = jsdata?.gameToken; + // Socket.IO automatically deserializes JSON - data is already an object + const gameToken = data?.gameToken; if (!gameToken) { logError('Game join failed: No game token provided'); @@ -243,6 +242,12 @@ export class GameWebSocketService { const isGamemaster = game.createdby === userId; const needsApproval = game.logintype === LoginType.PRIVATE && !isGamemaster; + logOther(`Player joining game: ${playerName}`); + logOther(` - userId: ${userId}`); + logOther(` - game.createdby: ${game.createdby}`); + logOther(` - isGamemaster: ${isGamemaster}`); + logOther(` - needsApproval: ${needsApproval}`); + // Generate dynamic room names (needed for both approval and direct join) const gameRoomName = `game_${gameCode}`; const playerRoomName = `game_${gameCode}:${playerName}`; @@ -275,6 +280,8 @@ export class GameWebSocketService { await socket.join(gameRoomName); await socket.join(playerRoomName); + // Update Redis with active player connection FIRST (before getting state) + await this.updatePlayerConnection(gameCode, playerName, true); // Send success response to the joining player socket.emit('game:joined', { @@ -296,13 +303,12 @@ export class GameWebSocketService { }); - // Send current game state to the joining player + // Send current game state to the joining player (now includes this player) const gameState = await this.getGameState(gameCode); socket.emit('game:state', gameState); - - // Update Redis with active player connection - await this.updatePlayerConnection(gameCode, playerName, true); + // Broadcast updated game state to all other players so they see the new player + socket.to(gameRoomName).emit('game:state-update', gameState); } catch (error) { socket.emit('game:error', { @@ -314,7 +320,7 @@ export class GameWebSocketService { private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise { try { - const { gameCode } = JSON.parse(data as any); + const { gameCode } = data; const playerName = socket.playerName; // Validate we have the required data @@ -354,7 +360,7 @@ export class GameWebSocketService { private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise { try { - const { gameCode, action, data: actionData } = JSON.parse(data as any); + const { gameCode, action, data: actionData } = data; if (!socket.gameCode || socket.gameCode !== gameCode) { socket.emit('game:error', { message: 'You must be in the game to perform actions' }); @@ -396,7 +402,7 @@ export class GameWebSocketService { private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise { try { - const { gameCode, message } = JSON.parse(data as any); + const { gameCode, message } = data; if (!socket.gameCode || socket.gameCode !== gameCode) { socket.emit('game:error', { message: 'You must be in the game to chat' }); @@ -422,7 +428,7 @@ export class GameWebSocketService { private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise { try { - const { gameCode, ready } = JSON.parse(data as any); + const { gameCode, ready } = data; const gameRoomName = `game_${gameCode}`; // Update player ready status in Redis @@ -452,7 +458,7 @@ export class GameWebSocketService { private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise { try { - const { gameCode, playerName } = JSON.parse(data as any); + const { gameCode, playerName } = data; // Verify that the requesting socket is the gamemaster const game = await this.gameRepository.findByGameCode(gameCode); @@ -513,7 +519,7 @@ export class GameWebSocketService { private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise { try { - const { gameCode, playerName, reason } = JSON.parse(data as any); + const { gameCode, playerName, reason } = data; // Verify that the requesting socket is the gamemaster const game = await this.gameRepository.findByGameCode(gameCode); @@ -561,7 +567,7 @@ export class GameWebSocketService { private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise { try { - const { gameToken } = JSON.parse(data as any); + const { gameToken } = data; if (!gameToken) { socket.emit('game:error', { message: 'Game token is required' }); @@ -606,6 +612,9 @@ export class GameWebSocketService { logOther(`Approved player ${playerName} joined game room: ${gameRoomName}`); + // Update Redis with active player connection FIRST (before getting state) + await this.updatePlayerConnection(gameCode, playerName, true); + // Send success response to the joining player socket.emit('game:joined', { gameCode, @@ -624,12 +633,12 @@ export class GameWebSocketService { timestamp: new Date().toISOString() }); - // Send current game state to the joining player + // Send current game state to the joining player (now includes this player) const gameState = await this.getGameState(gameCode); socket.emit('game:state', gameState); - // Update Redis with active player connection - await this.updatePlayerConnection(gameCode, playerName, true); + // Broadcast updated game state to all other players so they see the new player + socket.to(gameRoomName).emit('game:state-update', gameState); } catch (error) { logError('Error handling approved join', error as Error); @@ -639,7 +648,7 @@ export class GameWebSocketService { private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise { try { - const { gameCode, diceValue } = JSON.parse(data as any); + const { gameCode, diceValue } = data; // Validate input if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { @@ -738,7 +747,7 @@ export class GameWebSocketService { private async handleCardAnswer(socket: AuthenticatedSocket, data: CardAnswerData): Promise { try { - const { gameCode, answer } = JSON.parse(data as any); + const { gameCode, answer } = data; // Validate input if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { @@ -848,7 +857,7 @@ export class GameWebSocketService { private async handleGamemasterDecision(socket: AuthenticatedSocket, data: GamemasterDecisionData): Promise { try { - const { gameCode, requestId, decision } = JSON.parse(data as any); + const { gameCode, requestId, decision } = data; // Validate input if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { diff --git a/SerpentRace_Frontend/src/App.jsx b/SerpentRace_Frontend/src/App.jsx index c2c6b655..57501676 100644 --- a/SerpentRace_Frontend/src/App.jsx +++ b/SerpentRace_Frontend/src/App.jsx @@ -23,6 +23,7 @@ import { ToastConfig } from "./components/Toastify/toastifyServices" // ✅ font import VerifyEmailPage from "./pages/Auth/VerifyEmailPage" import ChooseDeck from "./pages/Game/ChooseDeck" import PlayerSetup from "./pages/Game/PlayerSetup" +import GameModalsDemo from "./pages/Game/GameModalsDemo" function App() { const [isMobile, setIsMobile] = useState(false) diff --git a/SerpentRace_Frontend/src/hooks/useGameWebSocket.js b/SerpentRace_Frontend/src/hooks/useGameWebSocket.js index 2c5a0cea..b623350b 100644 --- a/SerpentRace_Frontend/src/hooks/useGameWebSocket.js +++ b/SerpentRace_Frontend/src/hooks/useGameWebSocket.js @@ -5,7 +5,7 @@ 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); +const logError = (...args) => console.error(...args); /** * Optimized WebSocket hook for game connection @@ -20,29 +20,53 @@ export const useGameWebSocket = (gameToken) => { const [error, setError] = useState(null); const [isGamemaster, setIsGamemaster] = useState(false); const [gameStarted, setGameStarted] = useState(false); + const [pendingPlayers, setPendingPlayers] = useState([]); // Players waiting for approval + const [approvalStatus, setApprovalStatus] = useState(null); // 'pending' | 'approved' | 'denied' | null 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.) + // players: array of player IDs (UUIDs) - NOT USEFUL for display + // currentPlayers: 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; + // Debug: log what we received + if (import.meta.env.DEV) { + console.log('🎮 Computing players list:'); + console.log(' - connectedPlayers:', connectedPlayers); + console.log(' - currentPlayers:', currentPlayers); + } - // 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]); + // If we have full player objects with positions, use those (during game) + if (currentPlayers.length > 0) { + console.log('✅ Using currentPlayers'); + return currentPlayers; + } + + // Otherwise, use connectedPlayers (player names from Redis) + if (connectedPlayers.length > 0) { + console.log('✅ Mapping connectedPlayers to player objects'); + return connectedPlayers.map((nameOrObj, index) => { + // Handle both string names and objects + 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, + }; + }); + } + + console.log('⚠️ No players found'); + return []; + }, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]); const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]); // Connect to game WebSocket - only once per token @@ -83,44 +107,92 @@ export const useGameWebSocket = (gameToken) => { // Game state handlers - batch updates const handleGameState = (state) => { - log('📊 Game state:', state); + log('📊 Game state received:', state); + log(' - connectedPlayers:', state?.connectedPlayers); + log(' - players:', state?.players); + log(' - currentPlayers:', state?.currentPlayers); + log(' - isGamemaster in state:', state?.isGamemaster); + + // EXTRA DEBUG: Show full state structure + if (import.meta.env.DEV) { + console.log('🔍 FULL STATE OBJECT:', JSON.stringify(state, null, 2)); + } + + // If state contains isGamemaster flag, update it + if (state?.isGamemaster !== undefined) { + log('✅ Setting isGamemaster from state:', state.isGamemaster); + setIsGamemaster(state.isGamemaster); + } + setGameState(state); }; const handleGameJoined = (data) => { log('✅ Joined game:', data); + + // EXTRA DEBUG: Show full joined data + if (import.meta.env.DEV) { + console.log('🔍 FULL JOINED DATA:', JSON.stringify(data, null, 2)); + } + // Store if this user is the gamemaster if (data.isGamemaster !== undefined) { + log('✅ Setting isGamemaster from joined event:', data.isGamemaster); setIsGamemaster(data.isGamemaster); + } else { + log('⚠️ No isGamemaster flag in joined event'); } // Backend will send game:state next }; const handlePlayerJoined = (data) => { log('👤 Player joined:', data.playerName); + + // EXTRA DEBUG + if (import.meta.env.DEV) { + console.log('🔍 PLAYER JOINED EVENT:', JSON.stringify(data, null, 2)); + } + // Update game state to add the new player to connectedPlayers setGameState(prev => { - if (!prev) return prev; + if (!prev) { + log('⚠️ No previous game state, cannot add player'); + return prev; + } const currentConnected = prev.connectedPlayers || []; // Only add if not already in the list if (!currentConnected.includes(data.playerName)) { + log('✅ Adding player to connectedPlayers:', data.playerName); + log(' - Current list:', currentConnected); + log(' - New list:', [...currentConnected, data.playerName]); return { ...prev, connectedPlayers: [...currentConnected, data.playerName] }; } + log('⚠️ Player already in connectedPlayers:', 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); + + // EXTRA DEBUG + if (import.meta.env.DEV) { + console.log('🔍 GAME STARTED EVENT:', JSON.stringify(data, null, 2)); + } + // Signal that game has started setGameStarted(true); + + // Request updated game state from server (includes boardData and currentPlayers) + const socket = socketRef.current; + if (socket && socket.connected) { + log('📡 Requesting updated game state after game start'); + // The server will send game:state event with full data + } }; const handlePlayerMoved = (moveData) => { @@ -145,10 +217,68 @@ export const useGameWebSocket = (gameToken) => { }; const handleError = (err) => { - error('❌ Game error:', err); + logError('❌ Game error:', err); setError(err.message); }; + // Approval system handlers (PRIVATE games only) + const handlePendingApproval = (data) => { + log('⏳ Pending gamemaster approval:', data); + setApprovalStatus('pending'); + setError('Waiting for gamemaster approval...'); + }; + + const handlePlayerRequestingJoin = (data) => { + log('🔔 Player requesting to join:', data.playerName); + // Add to pending players list (for gamemaster) + setPendingPlayers(prev => { + if (prev.some(p => p.playerName === data.playerName)) return prev; + return [...prev, { + playerName: data.playerName, + isAuthenticated: data.isAuthenticated, + timestamp: data.timestamp + }]; + }); + }; + + const handleApprovalGranted = (data) => { + log('✅ Join request approved:', data); + setApprovalStatus('approved'); + setError(null); + + // Player should now join the game rooms + const socket = socketRef.current; + if (socket && data.gameRoomName) { + // Emit join-approved to notify backend + socket.emit('game:join-approved', { gameToken }); + } + }; + + const handleApprovalDenied = (data) => { + error('❌ Join request denied:', data.reason || data.message); + setApprovalStatus('denied'); + setError(data.reason || 'Your request to join was denied'); + }; + + const handlePlayerApproved = (data) => { + log('✅ Player approved by gamemaster:', data.playerName); + // Remove from pending players list + setPendingPlayers(prev => prev.filter(p => p.playerName !== data.playerName)); + + // Add to connected players + setGameState(prev => { + if (!prev) return prev; + const currentConnected = prev.connectedPlayers || []; + if (!currentConnected.includes(data.playerName)) { + return { + ...prev, + connectedPlayers: [...currentConnected, data.playerName] + }; + } + return prev; + }); + }; + // Register all handlers socket.on('connect', handleConnect); socket.on('connect_error', handleConnectError); @@ -161,6 +291,13 @@ export const useGameWebSocket = (gameToken) => { socket.on('game:player-moved', handlePlayerMoved); socket.on('game:turn-changed', handleTurnChanged); socket.on('game:error', handleError); + + // Approval system events (PRIVATE games) + socket.on('game:pending-approval', handlePendingApproval); + socket.on('game:player-requesting-join', handlePlayerRequestingJoin); + socket.on('game:approval-granted', handleApprovalGranted); + socket.on('game:approval-denied', handleApprovalDenied); + socket.on('game:player-approved', handlePlayerApproved); // Cleanup return () => { @@ -247,6 +384,115 @@ export const useGameWebSocket = (gameToken) => { return true; }, [isConnected, gameState?.gameCode]); + // Joker approval methods + const approveJoker = useCallback((playerId, cardId, requestId) => { + const socket = socketRef.current; + if (!socket || !isConnected) { + warn('⚠️ Cannot approve joker: not connected'); + return false; + } + + log('✅ Approving joker for player:', playerId); + socket.emit('game:gamemaster-decision', { + gameCode: gameState?.gameCode, + requestId: requestId || `joker-${playerId}-${cardId}`, + decision: 'approve', + }); + return true; + }, [isConnected, gameState?.gameCode]); + + const rejectJoker = useCallback((playerId, cardId, requestId) => { + const socket = socketRef.current; + if (!socket || !isConnected) { + warn('⚠️ Cannot reject joker: not connected'); + return false; + } + + log('❌ Rejecting joker for player:', playerId); + socket.emit('game:gamemaster-decision', { + gameCode: gameState?.gameCode, + requestId: requestId || `joker-${playerId}-${cardId}`, + decision: 'reject', + }); + return true; + }, [isConnected, gameState?.gameCode]); + + // Card answer submission + const submitAnswer = useCallback((cardId, answer) => { + const socket = socketRef.current; + if (!socket || !isConnected) { + warn('⚠️ Cannot submit answer: not connected'); + return false; + } + + log('📝 Submitting answer:', answer); + socket.emit('game:card-answer', { + gameCode: gameState?.gameCode, + cardId, + answer, + }); + return true; + }, [isConnected, gameState?.gameCode]); + + // Position guess submission + const submitPositionGuess = useCallback((guessedPosition) => { + const socket = socketRef.current; + if (!socket || !isConnected) { + warn('⚠️ Cannot submit position guess: not connected'); + return false; + } + + log('🎯 Submitting position guess:', guessedPosition); + socket.emit('game:position-guess', { + gameCode: gameState?.gameCode, + guessedPosition, + }); + return true; + }, [isConnected, gameState?.gameCode]); + + // Approve player (gamemaster only, PRIVATE games) + const approvePlayer = useCallback((playerName) => { + const socket = socketRef.current; + if (!socket || !isConnected) { + warn('⚠️ Cannot approve player: not connected'); + return false; + } + + if (!isGamemaster) { + warn('⚠️ Only gamemaster can approve players'); + return false; + } + + log('✅ Approving player:', playerName); + socket.emit('game:approve-player', { + gameCode: gameState?.gameCode, + playerName, + }); + return true; + }, [isConnected, isGamemaster, gameState?.gameCode]); + + // Reject player (gamemaster only, PRIVATE games) + const rejectPlayer = useCallback((playerName, reason = 'Join request denied') => { + const socket = socketRef.current; + if (!socket || !isConnected) { + warn('⚠️ Cannot reject player: not connected'); + return false; + } + + if (!isGamemaster) { + warn('⚠️ Only gamemaster can reject players'); + return false; + } + + log('❌ Rejecting player:', playerName); + socket.emit('game:reject-player', { + gameCode: gameState?.gameCode, + playerName, + reason, + }); + return true; + }, [isConnected, isGamemaster, gameState?.gameCode]); + return { socket: socketRef.current, isConnected, @@ -257,11 +503,19 @@ export const useGameWebSocket = (gameToken) => { error, isGamemaster, gameStarted, + pendingPlayers, + approvalStatus, // Methods rollDice, sendMessage, setReady, leaveGame, + approveJoker, + rejectJoker, + submitAnswer, + submitPositionGuess, + approvePlayer, + rejectPlayer, addEventListener, removeEventListener, }; diff --git a/SerpentRace_Frontend/src/pages/Game/CardDisplayModal.jsx b/SerpentRace_Frontend/src/pages/Game/CardDisplayModal.jsx new file mode 100644 index 00000000..a4f016e4 --- /dev/null +++ b/SerpentRace_Frontend/src/pages/Game/CardDisplayModal.jsx @@ -0,0 +1,280 @@ +import React, { useState, useEffect } from "react" +import { motion, AnimatePresence } from "framer-motion" + +/** + * CardDisplayModal - Kártya megjelenítése a játékos számára + * + * @param {Object} props + * @param {boolean} props.isOpen - Modal megjelenítése + * @param {Function} props.onClose - Modal bezárása + * @param {Object} props.card - Kártya adatok + * @param {string} props.cardType - Kártya típusa (QUESTION, LUCK, JOKER) + * @param {Function} props.onSubmitAnswer - Válasz beküldése (csak QUESTION típusnál) + * @param {number} props.timeLimit - Időkorlát másodpercben (default: 60) + */ +const CardDisplayModal = ({ + isOpen, + onClose, + card, + cardType = "QUESTION", + onSubmitAnswer, + timeLimit = 60 +}) => { + const [playerAnswer, setPlayerAnswer] = useState("") + const [selectedOption, setSelectedOption] = useState(null) + const [timeLeft, setTimeLeft] = useState(timeLimit) + const [isProcessing, setIsProcessing] = useState(false) + + // Timer countdown + useEffect(() => { + if (!isOpen || cardType !== "QUESTION") return + + setTimeLeft(timeLimit) + const timer = setInterval(() => { + setTimeLeft(prev => { + if (prev <= 1) { + clearInterval(timer) + handleTimeout() + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => clearInterval(timer) + }, [isOpen, timeLimit]) + + // Reset state when modal opens + useEffect(() => { + if (isOpen) { + setPlayerAnswer("") + setSelectedOption(null) + setIsProcessing(false) + } + }, [isOpen]) + + const handleTimeout = () => { + if (onSubmitAnswer) { + onSubmitAnswer(null) // null = timeout + } + } + + const handleSubmit = async () => { + if (isProcessing) return + + let answer = null + + // Quiz típus - A, B, C, D + if (card?.type === 0 || card?.answerOptions) { + answer = selectedOption + } + // Szöveges válasz + else { + answer = playerAnswer.trim() + } + + if (!answer) return + + setIsProcessing(true) + + try { + await onSubmitAnswer(answer) + } catch (error) { + console.error("Válasz küldési hiba:", error) + } + } + + const getCardIcon = () => { + switch (cardType) { + case "QUESTION": return "❓" + case "LUCK": return "🍀" + case "JOKER": return "🃏" + default: return "📝" + } + } + + const getCardTitle = () => { + switch (cardType) { + case "QUESTION": return "Feladat Kártya" + case "LUCK": return "Szerencse Kártya" + case "JOKER": return "Joker Kártya" + default: return "Kártya" + } + } + + const getCardBgGradient = () => { + switch (cardType) { + case "QUESTION": return "from-blue-600 via-purple-600 to-blue-600" + case "LUCK": return "from-green-600 via-teal-600 to-green-600" + case "JOKER": return "from-purple-600 via-pink-600 to-purple-600" + default: return "from-gray-600 via-gray-700 to-gray-600" + } + } + + const formatTime = (seconds) => { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins}:${secs.toString().padStart(2, '0')}` + } + + const getTimeColor = () => { + if (timeLeft > 30) return "text-green-400" + if (timeLeft > 10) return "text-yellow-400" + return "text-red-400 animate-pulse" + } + + if (!isOpen || !card) return null + + return ( + + {isOpen && ( +
+ {/* Backdrop */} + + + {/* Modal Content */} + + {/* Header */} +
+
+
+
+
{getCardIcon()}
+
+

{getCardTitle()}

+ {cardType === "QUESTION" && ( +

Válaszolj a kérdésre!

+ )} +
+
+ + {/* Timer - csak QUESTION típusnál */} + {cardType === "QUESTION" && ( +
+
+ ⏱️ {formatTime(timeLeft)} +
+
+ )} +
+
+ + {/* Content */} +
+ {/* Question/Text */} +
+
+
📝
+
+

+ {card.question || card.text || card.statement} +

+
+
+
+ + {/* Answer Options - Quiz típus (type: 0) */} + {cardType === "QUESTION" && (card.type === 0 || card.answerOptions) && ( +
+

Válaszd ki a helyes választ:

+ {card.answerOptions?.map((option, index) => ( + + ))} +
+ )} + + {/* Text Input - egyéb kérdés típusok */} + {cardType === "QUESTION" && card.type !== 0 && !card.answerOptions && ( +
+

Írd be a választ:

+ setPlayerAnswer(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSubmit()} + disabled={isProcessing} + placeholder="Válaszod..." + className="w-full bg-gray-800 border-2 border-gray-600 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none disabled:opacity-50" + /> +
+ )} + + {/* Hint (if available) */} + {card.hint && ( +
+
+
💡
+
+

Segítség

+

{card.hint}

+
+
+
+ )} + + {/* Submit Button - csak QUESTION típusnál */} + {cardType === "QUESTION" && ( + + )} + + {/* Close Button - LUCK és JOKER típusnál */} + {(cardType === "LUCK" || cardType === "JOKER") && ( + + )} +
+ +
+ )} + + ) +} + +export default CardDisplayModal diff --git a/SerpentRace_Frontend/src/pages/Game/ConsequenceModal.jsx b/SerpentRace_Frontend/src/pages/Game/ConsequenceModal.jsx new file mode 100644 index 00000000..72c242ae --- /dev/null +++ b/SerpentRace_Frontend/src/pages/Game/ConsequenceModal.jsx @@ -0,0 +1,202 @@ +import React from "react" +import { motion, AnimatePresence } from "framer-motion" + +/** + * ConsequenceModal - Következmények megjelenítése (jó/rossz válasz után) + * + * @param {Object} props + * @param {boolean} props.isOpen - Modal megjelenítése + * @param {Function} props.onClose - Modal bezárása + * @param {boolean} props.isCorrect - Helyes volt-e a válasz + * @param {string} props.consequence - Következmény szövege + * @param {number} props.consequenceType - Következmény típusa: + * 0 = MOVE_FORWARD (előre lépés) + * 1 = MOVE_BACKWARD (hátra lépés) + * 2 = LOSE_TURN (körkihagyás) + * 3 = EXTRA_TURN (extra kör) + * 5 = GO_TO_START (vissza a starthoz) + * @param {number} props.consequenceValue - Következmény értéke (hány mező/kör) + * @param {string} props.playerAnswer - Játékos válasza + * @param {string} props.correctAnswer - Helyes válasz + * @param {string} props.explanation - Magyarázat + */ +const ConsequenceModal = ({ + isOpen, + onClose, + isCorrect, + consequence, + consequenceType, + consequenceValue, + playerAnswer, + correctAnswer, + explanation +}) => { + + const getConsequenceIcon = (type) => { + switch(type) { + case 0: return "🚀" // MOVE_FORWARD + case 1: return "⬅️" // MOVE_BACKWARD + case 2: return "😴" // LOSE_TURN + case 3: return "🎉" // EXTRA_TURN + case 5: return "🏁" // GO_TO_START + default: return "📢" + } + } + + const getConsequenceText = (type, value) => { + switch(type) { + case 0: return `${value} mezőt léphetsz előre! 🚀` + case 1: return `${value} mezőt lépsz vissza! �` + case 2: return `${value} kört ki kell hagyni! �` + case 3: return `${value} extra kör jár neked! 🎉` + case 5: return "Vissza a starthoz! 🏁" + default: return consequence || "Következmény" + } + } + + const getBgGradient = () => { + if (isCorrect) { + return "from-green-600 via-teal-600 to-green-600" + } + return "from-red-600 via-orange-600 to-red-600" + } + + const getBorderColor = () => { + if (isCorrect) return "border-green-500/50" + return "border-red-500/50" + } + + if (!isOpen) return null + + return ( + + {isOpen && ( +
+ {/* Backdrop */} + + + {/* Modal Content */} + + {/* Header with result */} +
+
+ +
+ + {isCorrect ? "✅" : "❌"} + +

+ {isCorrect ? "Helyes válasz!" : "Helytelen válasz!"} +

+

+ {isCorrect ? "Gratulálunk! 🎉" : "Ne add fel! 💪"} +

+
+
+ + {/* Content */} +
+ {/* Player Answer */} + {playerAnswer && ( +
+
+
💭
+
+

A te válaszod:

+

{playerAnswer}

+
+
+
+ )} + + {/* Correct Answer - ha helytelen volt */} + {!isCorrect && correctAnswer && ( +
+
+
✔️
+
+

A helyes válasz:

+

{correctAnswer}

+
+
+
+ )} + + {/* Explanation */} + {explanation && ( +
+
+
💡
+
+

Magyarázat:

+

{explanation}

+
+
+
+ )} + + {/* Consequence */} + +
+ + {getConsequenceIcon(consequenceType)} + +

+ Következmény: +

+

+ {getConsequenceText(consequenceType, consequenceValue)} +

+
+
+ + {/* Close Button */} + +
+ +
+ )} + + ) +} + +export default ConsequenceModal diff --git a/SerpentRace_Frontend/src/pages/Game/GameModalsDemo.jsx b/SerpentRace_Frontend/src/pages/Game/GameModalsDemo.jsx new file mode 100644 index 00000000..0b29fc3b --- /dev/null +++ b/SerpentRace_Frontend/src/pages/Game/GameModalsDemo.jsx @@ -0,0 +1,250 @@ +import React, { useState } from "react" +import CardDisplayModal from "./CardDisplayModal" +import ConsequenceModal from "./ConsequenceModal" +import StepPredictionModal from "./StepPredictionModal" + +/** + * Demo oldal a játék modal-ok tesztelésére + */ +const GameModalsDemo = () => { + // Card Display Modal + const [isCardModalOpen, setIsCardModalOpen] = useState(false) + const [cardType, setCardType] = useState("QUESTION") + + // Consequence Modal + const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false) + const [isCorrect, setIsCorrect] = useState(true) + + // Step Prediction Modal + const [isStepModalOpen, setIsStepModalOpen] = useState(false) + + // Example cards + const quizCard = { + type: 0, + question: "Mi Magyarország fővárosa?", + answerOptions: [ + { answer: "A", text: "Debrecen", correct: false }, + { answer: "B", text: "Budapest", correct: true }, + { answer: "C", text: "Szeged", correct: false }, + { answer: "D", text: "Pécs", correct: false } + ], + hint: "A Duna partján fekszik" + } + + const textCard = { + type: 2, + question: "Hány éves vagy?", + hint: "Számmal válaszolj" + } + + const luckCard = { + text: "Szerencsés vagy! +2 lépés előre! 🍀", + consequence: { type: 3, value: 2 } + } + + const handleCardAnswer = (answer) => { + console.log("Válasz:", answer) + setIsCardModalOpen(false) + // Következmény megjelenítése + setTimeout(() => { + setIsCorrect(answer === "B") + setIsConsequenceModalOpen(true) + }, 500) + } + + const handleStepPrediction = (prediction) => { + console.log("Tipp:", prediction) + setIsStepModalOpen(false) + + // Példa: Számított pozíció = 20 + 4 + 2 + 2 = 28 + const actualPosition = 28 + const isCorrect = prediction === actualPosition + + // Következmény megjelenítése + setTimeout(() => { + setIsCorrect(isCorrect) + setIsConsequenceModalOpen(true) + }, 500) + } + + return ( +
+
+

+ 🎮 Játék Modal-ok Demo +

+ +
+ {/* Card Display Modal Demos */} +
+

+ 📝 Kártya Megjelenítés +

+
+ + + +
+
+ + {/* Consequence Modal Demos */} +
+

+ 🎯 Következmények +

+
+ + +
+
+ + {/* Step Prediction Modal Demo */} +
+

+ 🎲 Pozíció Tippelés +

+ +

+ Tipppeld meg a végleges pozíciót a számítás alapján! +

+
+
+ + {/* Info Panel */} +
+

ℹ️ Használat a GameScreen-ben

+
+
+{`// Import-ok
+import CardDisplayModal from "./CardDisplayModal"
+import ConsequenceModal from "./ConsequenceModal"
+import StepPredictionModal from "./StepPredictionModal"
+
+// State-ek
+const [isCardModalOpen, setIsCardModalOpen] = useState(false)
+const [currentCard, setCurrentCard] = useState(null)
+const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false)
+const [consequenceData, setConsequenceData] = useState(null)
+
+// WebSocket event
+useEffect(() => {
+  if (socket) {
+    socket.on('game:card-drawn', (data) => {
+      setCurrentCard(data.card)
+      setIsCardModalOpen(true)
+    })
+    
+    socket.on('game:answer-result', (data) => {
+      setConsequenceData(data)
+      setIsConsequenceModalOpen(true)
+    })
+  }
+}, [socket])
+
+// Render
+ setIsCardModalOpen(false)}
+  card={currentCard}
+  cardType="QUESTION"
+  onSubmitAnswer={handleSubmitAnswer}
+/>
+
+ setIsConsequenceModalOpen(false)}
+  {...consequenceData}
+/>`}
+            
+
+
+
+ + {/* Modals */} + setIsCardModalOpen(false)} + card={cardType === "LUCK" ? luckCard : quizCard} + cardType={cardType} + onSubmitAnswer={handleCardAnswer} + /> + + setIsConsequenceModalOpen(false)} + isCorrect={isCorrect} + consequenceType={isCorrect ? 3 : 4} + consequenceValue={isCorrect ? 2 : 1} + playerAnswer={isCorrect ? "Budapest" : "Debrecen"} + correctAnswer="Budapest" + explanation={isCorrect + ? "Budapest a magyar főváros, 1873-ban egyesült Buda, Pest és Óbuda városa." + : "A helyes válasz Budapest. Debrecen Magyarország második legnagyobb városa." + } + /> + + setIsStepModalOpen(false)} + onSubmitPrediction={handleStepPrediction} + currentPosition={20} + diceRoll={4} + fieldStepValue={2} + patternModifier={2} + cardText="Tippeld meg, melyik pozícióra fogsz lépni a számítás alapján!" + hints={[ + "A végső pozíció = jelenlegi pozíció + dobás + mező lépés + zóna módosító", + "Ebben a példában: 20 + 4 + 2 + 2 = 28", + "A zóna módosító a pozíció alapján változik!" + ]} + /> +
+ ) +} + +export default GameModalsDemo diff --git a/SerpentRace_Frontend/src/pages/Game/GameScreen.jsx b/SerpentRace_Frontend/src/pages/Game/GameScreen.jsx index 107ef68e..f9cf1592 100644 --- a/SerpentRace_Frontend/src/pages/Game/GameScreen.jsx +++ b/SerpentRace_Frontend/src/pages/Game/GameScreen.jsx @@ -2,6 +2,10 @@ import React, { useState, useEffect, useMemo, useCallback } from "react" import { getVerticalOffset } from "../../utils/randomUtils" import Dice from "../../utils/dice/Dice" import { useGameWebSocket } from "../../hooks/useGameWebSocket" +import JokerApprovalModal from "./JokerApprovalModal" +import CardDisplayModal from "./CardDisplayModal" +import ConsequenceModal from "./ConsequenceModal" +import StepPredictionModal from "./StepPredictionModal" // Constants - outside component to prevent recreation const PLAYER_STYLES = [ @@ -47,16 +51,53 @@ const GameScreen = () => { isConnected, gameState, players: backendPlayers, - boardData, + boardData: websocketBoardData, currentTurn, error, rollDice, + approveJoker, + rejectJoker, + submitAnswer, + submitPositionGuess, addEventListener, removeEventListener } = useGameWebSocket(gameToken) + // Try to get boardData from WebSocket, fallback to localStorage + const boardData = useMemo(() => { + if (websocketBoardData) return websocketBoardData + + try { + const stored = localStorage.getItem('boardData') + if (stored) { + console.log('📦 Loading boardData from localStorage') + return JSON.parse(stored) + } + } catch (err) { + console.error('Failed to parse boardData from localStorage:', err) + } + + return null + }, [websocketBoardData]) + const [path, setPath] = useState([]) const [players, setPlayers] = useState([]) + + // Joker approval modal state + const [isJokerModalOpen, setIsJokerModalOpen] = useState(false) + const [currentJokerRequest, setCurrentJokerRequest] = useState(null) + + // Card display modal state + const [isCardModalOpen, setIsCardModalOpen] = useState(false) + const [currentCard, setCurrentCard] = useState(null) + + // Consequence modal state + const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false) + const [currentConsequence, setCurrentConsequence] = useState(null) + + // Step prediction modal state + const [isPredictionModalOpen, setIsPredictionModalOpen] = useState(false) + const [currentPredictionData, setCurrentPredictionData] = useState(null) // Memoized board dimensions const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => { @@ -171,6 +212,178 @@ const GameScreen = () => { return () => removeEventListener('game:player-moved') }, [addEventListener, removeEventListener]) + // Listen to Joker card events (csak Gamemaster számára) + useEffect(() => { + if (!addEventListener) return + + const handleJokerDrawn = (jokerData) => { + console.log('🃏 Joker kártya húzva:', jokerData) + // Joker approval modal megjelenítése + setCurrentJokerRequest({ + playerId: jokerData.playerId, + playerName: jokerData.playerName, + playerEmoji: jokerData.playerEmoji || "🎭", + cardTitle: jokerData.cardTitle || jokerData.jokerCard?.question, + cardDescription: jokerData.cardDescription || jokerData.jokerCard?.consequence?.description, + points: jokerData.points || jokerData.jokerCard?.consequence?.value, + cardId: jokerData.cardId || jokerData.jokerCard?.id, + requestId: jokerData.requestId, // Important: requestId from backend + timestamp: Date.now() + }) + setIsJokerModalOpen(true) + } + + // Listen for gamemaster decision request (correct event name per docs) + addEventListener('game:joker-drawn', handleJokerDrawn) + addEventListener('game:gamemaster-decision-request', handleJokerDrawn) + + return () => { + removeEventListener('game:joker-drawn') + removeEventListener('game:gamemaster-decision-request') + } + }, [addEventListener, removeEventListener]) + + // Listen to card drawn events (kártya megjelenítés) + useEffect(() => { + if (!addEventListener) return + + const handleCardDrawn = (cardData) => { + console.log('🎴 Kártya húzva:', cardData) + setCurrentCard({ + id: cardData.cardId || cardData.id, + type: cardData.cardType || cardData.type, + question: cardData.question || cardData.text, + answerOptions: cardData.answerOptions || cardData.options || [], + correctAnswer: cardData.correctAnswer, + points: cardData.points || 0, + timeLimit: cardData.timeLimit || 60 + }) + setIsCardModalOpen(true) + } + + // Listen for both generic and self-specific events + addEventListener('game:card-drawn', handleCardDrawn) + addEventListener('game:card-drawn-self', handleCardDrawn) + + return () => { + removeEventListener('game:card-drawn') + removeEventListener('game:card-drawn-self') + } + }, [addEventListener, removeEventListener]) + + // Listen to answer validation (következmény megjelenítés) + useEffect(() => { + if (!addEventListener) return + + const handleAnswerValidated = (resultData) => { + console.log('✅ Válasz kiértékelve:', resultData) + + // Close card modal first + setIsCardModalOpen(false) + + // Show consequence modal + setCurrentConsequence({ + isCorrect: resultData.isCorrect || resultData.correct, + playerAnswer: resultData.playerAnswer || resultData.answer, + correctAnswer: resultData.correctAnswer, + explanation: resultData.explanation || '', + consequenceType: resultData.consequenceType || resultData.consequence?.type, + consequenceValue: resultData.consequenceValue || resultData.consequence?.value || 0, + points: resultData.pointsEarned || resultData.points || 0 + }) + setIsConsequenceModalOpen(true) + } + + // Also listen for luck consequences (instant consequences from luck cards) + const handleLuckConsequence = (luckData) => { + console.log('🍀 Szerencse kártya következménye:', luckData) + + setCurrentConsequence({ + isCorrect: true, // Luck cards don't have right/wrong answers + consequenceType: luckData.consequenceType, + consequenceValue: luckData.value || luckData.consequenceValue || 0, + explanation: luckData.message || 'Szerencse kártya!', + playerAnswer: null, + correctAnswer: null + }) + setIsConsequenceModalOpen(true) + } + + addEventListener('game:answer-validated', handleAnswerValidated) + addEventListener('game:luck-consequence', handleLuckConsequence) + + return () => { + removeEventListener('game:answer-validated') + removeEventListener('game:luck-consequence') + } + }, [addEventListener, removeEventListener]) + + // Listen to position guess requests (lépés tippelés) + useEffect(() => { + if (!addEventListener) return + + const handlePositionGuessRequest = (predictionData) => { + console.log('🎯 Pozíció tippelés kérés:', predictionData) + setCurrentPredictionData({ + currentPosition: predictionData.currentPosition, + diceRoll: predictionData.diceRoll || predictionData.dice, + fieldStepValue: predictionData.fieldStepValue || predictionData.fieldStep || 0, + patternModifier: predictionData.patternModifier || predictionData.zoneModifier || 0, + cardText: predictionData.cardText || predictionData.text || 'Tippeld meg, hova fogsz lépni!', + timeLimit: predictionData.timeLimit || 30 + }) + setIsPredictionModalOpen(true) + } + + addEventListener('game:position-guess-request', handlePositionGuessRequest) + return () => removeEventListener('game:position-guess-request') + }, [addEventListener, removeEventListener]) + + // Joker jóváhagyás + const handleApproveJoker = useCallback(async (jokerRequest) => { + console.log('✅ Joker feladat jóváhagyva:', jokerRequest) + + // WebSocket üzenet a backend felé + approveJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId) + + // Modal bezárása + setIsJokerModalOpen(false) + }, [approveJoker]) + + // Joker elutasítás + const handleRejectJoker = useCallback(async (jokerRequest) => { + console.log('❌ Joker feladat elutasítva:', jokerRequest) + + // WebSocket üzenet a backend felé + rejectJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId) + + // Modal bezárása + setIsJokerModalOpen(false) + }, [rejectJoker]) + + // Kártya válasz beküldése + const handleSubmitAnswer = useCallback((answer) => { + console.log('📝 Válasz beküldve:', answer) + + // WebSocket emit a backend felé + if (currentCard?.id) { + submitAnswer(currentCard.id, answer) + } + + // A consequence modal automatikusan megnyílik a 'game:answer-validated' event hatására + }, [currentCard?.id, submitAnswer]) + + // Pozíció tippelés beküldése + const handleSubmitPrediction = useCallback((predictedPosition) => { + console.log('🎯 Pozíció tippelés beküldve:', predictedPosition) + + // WebSocket emit a backend felé + submitPositionGuess(predictedPosition) + + // Modal bezárása + setIsPredictionModalOpen(false) + }, [submitPositionGuess]) + // Sorted players - memoized const sortedPlayers = useMemo( () => [...players].sort((a, b) => b.position - a.position), @@ -179,7 +392,13 @@ const GameScreen = () => { // Handle dice roll const handleDiceRoll = useCallback((value) => { - rollDice(value) + console.log('🎲 Dobás:', value) + const success = rollDice(value) + if (success) { + console.log('✅ Kockadobás elküldve a szervernek') + } else { + console.warn('⚠️ Kockadobás sikertelen - nincs kapcsolat vagy nem te következel') + } }, [rollDice]) // Get field style - memoized @@ -229,18 +448,16 @@ const GameScreen = () => {
- - {isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'} - -
- {error && ( -
- ⚠️ {error} -
- )} + + {isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'} +
- - {/* Game Info Bar */} + {error && !error.includes('Game not found') && !error.includes('token invalid') && ( +
+ ⚠️ {error} +
+ )} + {/* Game Info Bar */} {gameState && (
@@ -416,6 +633,40 @@ const GameScreen = () => {
+ + {/* Joker Approval Modal - csak Gamemaster számára */} + setIsJokerModalOpen(false)} + jokerRequest={currentJokerRequest} + onApprove={handleApproveJoker} + onReject={handleRejectJoker} + playerName={currentJokerRequest?.playerName} + playerEmoji={currentJokerRequest?.playerEmoji} + /> + + {/* Card Display Modal - kártya megjelenítés */} + setIsCardModalOpen(false)} + card={currentCard} + onSubmitAnswer={handleSubmitAnswer} + /> + + {/* Consequence Modal - következmények megjelenítése */} + setIsConsequenceModalOpen(false)} + consequence={currentConsequence} + /> + + {/* Step Prediction Modal - pozíció tippelés */} + setIsPredictionModalOpen(false)} + predictionData={currentPredictionData} + onSubmitPrediction={handleSubmitPrediction} + /> ) } diff --git a/SerpentRace_Frontend/src/pages/Game/JokerApprovalModal.jsx b/SerpentRace_Frontend/src/pages/Game/JokerApprovalModal.jsx new file mode 100644 index 00000000..30671cf0 --- /dev/null +++ b/SerpentRace_Frontend/src/pages/Game/JokerApprovalModal.jsx @@ -0,0 +1,222 @@ +import React, { useState } from "react" +import { motion, AnimatePresence } from "framer-motion" + +/** + * JokerApprovalModal - Gamemaster felület a Joker kártya feladatok jóváhagyására + * + * @param {Object} props + * @param {boolean} props.isOpen - Modal megjelenítése + * @param {Function} props.onClose - Modal bezárása + * @param {Object} props.jokerRequest - Joker kártya adatok + * @param {Function} props.onApprove - Jóváhagyás callback + * @param {Function} props.onReject - Elutasítás callback + * @param {string} props.playerName - Játékos neve + * @param {string} props.playerEmoji - Játékos emoji + */ +const JokerApprovalModal = ({ + isOpen, + onClose, + jokerRequest, + onApprove, + onReject, + playerName, + playerEmoji = "🎭" +}) => { + const [isProcessing, setIsProcessing] = useState(false) + + const handleApprove = async () => { + setIsProcessing(true) + try { + await onApprove(jokerRequest) + onClose() + } catch (error) { + console.error("Jóváhagyási hiba:", error) + } finally { + setIsProcessing(false) + } + } + + const handleReject = async () => { + setIsProcessing(true) + try { + await onReject(jokerRequest) + onClose() + } catch (error) { + console.error("Elutasítási hiba:", error) + } finally { + setIsProcessing(false) + } + } + + if (!isOpen) return null + + return ( + + {isOpen && ( +
+ {/* Backdrop */} + + + {/* Modal Content */} + + {/* Header with Joker theme */} +
+
+
+
+
🃏
+
+

Joker Kártya Feladat

+

Gamemaster jóváhagyás szükséges

+
+
+ +
+
+ + {/* Content */} +
+ {/* Player Info */} +
+
+
{playerEmoji}
+
+

Játékos

+

{playerName}

+
+
+
+ + {/* Joker Card Details */} +
+
+
🎯
+
+

Feladat címe

+

+ {jokerRequest?.cardTitle || "Joker Kártya Feladat"} +

+
+
+ +
+
📝
+
+

Feladat leírása

+

+ {jokerRequest?.cardDescription || "A játékosnak teljesítenie kell a Joker kártya feladatát."} +

+
+
+ + {/* Points Info */} + {jokerRequest?.points && ( +
+
+ + + {jokerRequest.points} pont + + járható érte +
+
+ )} +
+ + {/* Player's Claim (Optional - ha később hozzáadod) */} + {jokerRequest?.playerMessage && ( +
+
+
💬
+
+

Játékos üzenete

+

"{jokerRequest.playerMessage}"

+
+
+
+ )} + + {/* Instructions */} +
+
+
ℹ️
+
+

+ Gamemaster döntés: Nézd meg, hogy a játékos teljesítette-e a feladatot, + majd hagyd jóvá vagy utasítsd el. +

+
+
+
+ + {/* Action Buttons */} +
+ + + +
+ + {/* Processing indicator */} + {isProcessing && ( +
+
+
⚙️
+ Feldolgozás... +
+
+ )} +
+ +
+ )} + + ) +} + +export default JokerApprovalModal diff --git a/SerpentRace_Frontend/src/pages/Game/Lobby.jsx b/SerpentRace_Frontend/src/pages/Game/Lobby.jsx index b4a73060..3f6768ef 100644 --- a/SerpentRace_Frontend/src/pages/Game/Lobby.jsx +++ b/SerpentRace_Frontend/src/pages/Game/Lobby.jsx @@ -9,6 +9,7 @@ import { startGame } from "../../api/gameApi.js" const Lobby = () => { const [visible, setVisible] = useState(false) + const [isStarting, setIsStarting] = useState(false) const sectionRef = useRef(null) const { goHome, goGame } = HandleNavigate() const location = useLocation() @@ -25,19 +26,29 @@ const Lobby = () => { players, isGamemaster, gameStarted, + pendingPlayers, + approvalStatus, + approvePlayer, + rejectPlayer, } = useGameWebSocket(gameToken) const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...' - // Filter out gamemaster from player list - gamemaster is NOT a player - const currentPlayers = (players || []).filter(p => { - // If we have userId info, filter by that - if (p.userId) { - return p.userId !== gameState?.createdBy + // Players list - gamemaster is separate, don't filter + // Backend should handle this correctly + const currentPlayers = players || [] + + // Debug logging + useEffect(() => { + if (import.meta.env.DEV) { + console.log('🎮 Lobby state update:') + console.log(' - isGamemaster:', isGamemaster) + console.log(' - gameState:', gameState) + console.log(' - players:', players) + console.log(' - currentPlayers:', currentPlayers) + console.log(' - pendingPlayers:', pendingPlayers) } - // Otherwise filter by name (less reliable but works for now) - return true - }) + }, [isGamemaster, gameState, players, currentPlayers, pendingPlayers]) useEffect(() => { const observer = new IntersectionObserver( @@ -56,7 +67,18 @@ const Lobby = () => { console.log('🎮 Game started, navigating to /game') goGame() } - }, [gameStarted, goGame]) + }, [gameStarted, navigate]) + + // Handle approval status changes + useEffect(() => { + if (approvalStatus === 'denied') { + alert('A gamemaster elutasította a csatlakozási kérelmedet.') + localStorage.removeItem('gameToken') + navigate("/home") + } else if (approvalStatus === 'approved') { + console.log('✅ Join approved, you can now see the lobby') + } + }, [approvalStatus, navigate]) const handleExit = () => { if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) { @@ -66,7 +88,15 @@ const Lobby = () => { } const handleStartGame = async () => { + // Prevent double-click + if (isStarting) { + console.log('⚠️ Game start already in progress, ignoring duplicate request') + return + } + try { + setIsStarting(true) + // Get gameId from gameState const gameId = gameState?.gameId if (!gameId) { @@ -78,12 +108,29 @@ const Lobby = () => { const response = await startGame(gameId) console.log('Game start response:', response) - // Backend will broadcast game:started event to all players - // Navigate to game page - goGame() + // Store boardData and updated game state for GameScreen + if (response.boardData) { + localStorage.setItem('boardData', JSON.stringify(response.boardData)) + console.log('✅ boardData stored in localStorage') + } + + // Navigate immediately after successful start (don't wait for WebSocket) + console.log('🎮 Navigating to /game...') + navigate('/game') + } catch (error) { console.error('Failed to start game:', error) - alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`) + + // Check if game already started + if (error.response?.status === 409) { + console.log('Game already started, navigating to /game...') + // Navigate anyway - game is already running + navigate('/game') + } else { + alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`) + } + } finally { + setIsStarting(false) } } @@ -92,6 +139,21 @@ const Lobby = () => { alert('Játék kód vágólapra másolva: ' + gameCode) } + const handleApprovePlayer = (playerName) => { + if (approvePlayer(playerName)) { + console.log(`✅ Player ${playerName} approved`) + } + } + + const handleRejectPlayer = (playerName) => { + const reason = prompt(`Miért utasítod el ${playerName}-t?`, 'Nincs hely a játékban') + if (reason !== null) { // User didn't cancel + if (rejectPlayer(playerName, reason)) { + console.log(`❌ Player ${playerName} rejected`) + } + } + } + const getInitials = (name) => { return name .split(" ") @@ -111,7 +173,47 @@ const Lobby = () => {
-
+ {/* Waiting for Approval Screen (Non-gamemaster, PRIVATE games) */} + {!isGamemaster && approvalStatus === 'pending' && ( +
+
+
+
+
+ +
+
+

+ Várakozás jóváhagyásra +

+

+ A gamemaster még nem hagyta jóvá a csatlakozásodat. +

+

+ Kérjük, várj türelemmel, amíg a gamemaster elfogadja a kérelmedet. +

+
+
+

Játék kód:

+

+ {gameCode} +

+
+ +
+
+
+
+ )} + + {/* Normal Lobby View (Gamemaster or approved players) */} + {(isGamemaster || approvalStatus !== 'pending') && ( +
{ Játékosok ({currentPlayers.length}):

+ {/* Pending Players Section (Gamemaster only, PRIVATE games) */} + {isGamemaster && pendingPlayers && pendingPlayers.length > 0 && ( +
+

+ + Jóváhagyásra váró játékosok ({pendingPlayers.length}) +

+
    + {pendingPlayers.map((player, index) => ( +
  • +
    + {getInitials(player.playerName)} +
    +
    + + {player.playerName} + + {player.isAuthenticated && ( + + ✓ Bejelentkezve + + )} +
    +
    + + +
    +
  • + ))} +
+
+ )} +
    {currentPlayers.length === 0 ? ( @@ -215,15 +369,21 @@ const Lobby = () => { /* Gamemaster view - can start game */ ) : ( /* Player view - cannot start game, just wait */ @@ -240,7 +400,8 @@ const Lobby = () => {
-
+ + )} ) } diff --git a/SerpentRace_Frontend/src/pages/Game/StepPredictionModal.jsx b/SerpentRace_Frontend/src/pages/Game/StepPredictionModal.jsx new file mode 100644 index 00000000..d7db2918 --- /dev/null +++ b/SerpentRace_Frontend/src/pages/Game/StepPredictionModal.jsx @@ -0,0 +1,305 @@ +import React, { useState, useEffect } from "react" +import { motion, AnimatePresence } from "framer-motion" + +/** + * StepPredictionModal - Pozíció tippelés (Position Guessing) + * + * A dokumentáció szerint: A játékosnak meg kell tippelnie a VÉGLEGES POZÍCIÓT, + * nem a lépésszámot! + * + * Számítás: finalPosition = currentPosition + diceRoll + fieldStepValue + patternModifier + * + * @param {Object} props + * @param {boolean} props.isOpen - Modal megjelenítése + * @param {Function} props.onClose - Modal bezárása + * @param {Function} props.onSubmitPrediction - Tipp beküldése + * @param {number} props.currentPosition - Jelenlegi pozíció + * @param {number} props.diceRoll - Dobás értéke + * @param {number} props.fieldStepValue - Mező lépés értéke + * @param {number} props.patternModifier - Zóna alapú módosító + * @param {string} props.cardText - Kártya szövege + * @param {Array} props.hints - Segédletek tömbje + * @param {number} props.timeLimit - Időkorlát másodpercben (default: 30) + */ +const StepPredictionModal = ({ + isOpen, + onClose, + onSubmitPrediction, + currentPosition = 0, + diceRoll = 0, + fieldStepValue = 0, + patternModifier = 0, + cardText = "Tippeld meg, melyik pozícióra fogsz lépni!", + hints = [], + timeLimit = 30 +}) => { + const [prediction, setPrediction] = useState("") + const [showHints, setShowHints] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [timeLeft, setTimeLeft] = useState(timeLimit) + + // Timer countdown + useEffect(() => { + if (!isOpen) return + + setTimeLeft(timeLimit) + const timer = setInterval(() => { + setTimeLeft(prev => { + if (prev <= 1) { + clearInterval(timer) + handleTimeout() + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => clearInterval(timer) + }, [isOpen, timeLimit]) + + const handleTimeout = () => { + if (onSubmitPrediction) { + onSubmitPrediction(null) // null = timeout + } + } + + const handleSubmit = async () => { + if (!prediction || prediction === "" || isProcessing) return + + const guessedPosition = parseInt(prediction) + if (isNaN(guessedPosition)) return + + setIsProcessing(true) + + try { + await onSubmitPrediction(guessedPosition) + } catch (error) { + console.error("Tipp küldési hiba:", error) + setIsProcessing(false) + } + } + + // Reset amikor megnyílik + useEffect(() => { + if (isOpen) { + setPrediction("") + setShowHints(false) + setIsProcessing(false) + } + }, [isOpen]) + + // Számított végső pozíció (helyes válasz) + const calculatedPosition = currentPosition + diceRoll + fieldStepValue + patternModifier + + const formatTime = (seconds) => { + return `0:${seconds.toString().padStart(2, '0')}` + } + + const getTimeColor = () => { + if (timeLeft > 15) return "text-green-400" + if (timeLeft > 5) return "text-yellow-400" + return "text-red-400 animate-pulse" + } + + if (!isOpen) return null + + return ( + + {isOpen && ( +
+ {/* Backdrop */} + + + {/* Modal Content */} + + {/* Header */} +
+
+
+
+
🎯
+
+

Pozíció Tippelés

+

Melyik pozícióra lépsz?

+
+
+ + {/* Timer */} +
+
+ ⏱️ {formatTime(timeLeft)} +
+
+
+
+ + {/* Content */} +
+ {/* Card Text / Instructions */} +
+
+
📝
+
+

+ {cardText} +

+
+
+
+ + {/* Calculation Info */} +
+

📊 Számítási Adatok

+
+
+

Jelenlegi pozíció

+

{currentPosition}

+
+
+

Dobás (kocka)

+

+{diceRoll}

+
+
+

Mező lépés

+

+{fieldStepValue}

+
+
+

Zóna módosító

+

= 0 ? 'text-green-400' : 'text-red-400'}`}> + {patternModifier >= 0 ? '+' : ''}{patternModifier} +

+
+
+
+

+ Számítsd ki: {currentPosition} + {diceRoll} + {fieldStepValue} {patternModifier >= 0 ? '+' : ''} {patternModifier} = ? +

+
+
+ + {/* Position Input */} +
+

+ Írd be a tippelt pozíciót: +

+ setPrediction(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSubmit()} + disabled={isProcessing} + placeholder="Pl: 28" + className="w-full bg-gray-800 border-2 border-yellow-600 rounded-xl px-4 py-3 text-white text-center text-2xl font-bold focus:border-yellow-400 focus:outline-none disabled:opacity-50" + min={currentPosition} + max={100} + /> +
+ + {/* Prediction Info */} + {prediction && prediction !== "" && ( + +

+ A tipped: {prediction} pozíció +

+
+ )} + + {/* Hints Section */} + {hints && hints.length > 0 && ( +
+ + + + {showHints && ( + + {hints.map((hint, index) => ( +
+

+ #{index + 1}: {hint} +

+
+ ))} +
+ )} +
+
+ )} + + {/* Risk/Reward Info */} +
+
+
+
+

Ha eltalálod

+

Lépsz az új pozícióra!

+
+
+
+

Ha nem találod el

+

-2 büntetés + nem lépsz!

+
+
+
+ + {/* Submit Button */} + + + {/* Warning */} + {(!prediction || prediction === "") && ( +

+ ⚠️ Írd be a tippelt pozíciót a beküldéshez! +

+ )} +
+ +
+ )} + + ) +} + +export default StepPredictionModal