import React, { useState, useEffect, useMemo, useCallback } from "react" import { getVerticalOffset } from "../../utils/randomUtils" import Dice from "../../utils/dice/Dice" import { useGameWebSocketContext } from "../../contexts/GameWebSocketContext" 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 = [ { color: "bg-blue-600", emoji: "🐍" }, { color: "bg-green-600", emoji: "🐢" }, { color: "bg-purple-600", emoji: "🐇" }, { color: "bg-yellow-600", emoji: "🦊" }, { color: "bg-red-600", emoji: "🦁" }, { color: "bg-pink-600", emoji: "🐷" }, { color: "bg-orange-600", emoji: "🐯" }, { color: "bg-indigo-600", emoji: "🐺" }, ] const BOARD_CONFIG = { rows: 5, cols: 20, cellSize: 40, cellMargin: 2.5, rowSpacing: 70, } // Helper functions outside component const mapFieldType = (backendType) => { switch (backendType) { case 'positive': return 'good' case 'negative': return 'bad' case 'luck': return 'clover' default: return 'regular' } } const getDefaultFieldType = (count) => { if (count % 17 === 0) return "clover" if (count % 13 === 0) return "bad" if ((count + 5) % 13 === 0) return "good" return "regular" } const GameScreen = () => { // WebSocket connection from context (maintains connection across navigation) const { isConnected, gameState, players: backendPlayers, boardData: websocketBoardData, currentTurn, error, rollDice, approveJoker, rejectJoker, submitAnswer, submitPositionGuess, addEventListener, removeEventListener } = useGameWebSocketContext() // 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(() => { const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG const topOffset = rowSpacing * 0.5 const bottomOffset = rowSpacing * 0.5 const totalCells = rows * cols return { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, bottomOffset, width: cols * (cellSize + cellMargin * 2), height: rows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing, } }, []) // Memoized path generator - Snake pattern with proper turn handling const generateWindingPath = useCallback((backendFields = null) => { const path = [] const hasBackendData = backendFields && Array.isArray(backendFields) let currentNum = 1 // Generate all 100 positions while (currentNum <= totalCells) { const row = Math.floor((currentNum - 1) / cols) const posInRow = (currentNum - 1) % cols const isLeftToRight = row % 2 === 0 // Calculate column based on direction const col = isLeftToRight ? posInRow : (cols - 1 - posInRow) // Base Y position for this row let baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing) // Apply vertical offset for wave effect let yOffset = getVerticalOffset(currentNum - 1) // Special handling for turn positions (21, 41, 61, 81) // These should be positioned between rows to show the turn if (currentNum % cols === 1 && currentNum > 1) { // This is the first element of a new row (21, 41, 61, 81) // Position it halfway between the previous row and current row baseYPosition = topOffset + (row - 0.5) * (cellSize + cellMargin * 2 + rowSpacing) yOffset = 0 // Reset wave offset for turn positions } const backendField = hasBackendData ? backendFields.find(f => f.position === currentNum) : null path.push({ number: currentNum, x: col * (cellSize + cellMargin * 2), y: baseYPosition + yOffset, type: backendField ? mapFieldType(backendField.type) : getDefaultFieldType(currentNum - 1), stepValue: backendField?.stepValue || 0, }) currentNum++ } return path }, [rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset]) // Update path when boardData changes useEffect(() => { if (boardData?.fields) { setPath(generateWindingPath(boardData.fields)) } else if (path.length === 0) { setPath(generateWindingPath()) } }, [boardData, generateWindingPath]) // Update players from backend - memoized mapping useEffect(() => { if (!backendPlayers?.length) return const mappedPlayers = backendPlayers.map((player, index) => ({ id: player.playerId || player.id || index, name: player.playerName || player.name || `Player ${index + 1}`, position: player.boardPosition || 0, score: player.score || 0, color: PLAYER_STYLES[index % PLAYER_STYLES.length].color, emoji: PLAYER_STYLES[index % PLAYER_STYLES.length].emoji, isOnline: player.isOnline !== undefined ? player.isOnline : true, isReady: player.isReady || false, })) setPlayers(mappedPlayers) }, [backendPlayers]) // Listen to player movement - optimized to update only moved player useEffect(() => { if (!addEventListener) return const handlePlayerMoved = (moveData) => { setPlayers(prev => prev.map(p => p.id === moveData.playerId ? { ...p, position: moveData.newPosition } : p ) ) } addEventListener('game:player-moved', handlePlayerMoved) 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), [players] ) // Handle dice roll const handleDiceRoll = useCallback((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 const getFieldStyle = useCallback((type) => { switch (type) { case "clover": return "bg-teal-700 border-teal-500 shadow-teal-800" case "bad": return "bg-red-800 border-red-600 shadow-red-900" case "good": return "bg-blue-800 border-blue-600 shadow-blue-900" default: return "bg-gray-800 border-gray-600 shadow-gray-900" } }, []) // Get player position - memoized const getPlayerPosition = useCallback((playerPosition) => { const field = path.find((p) => p.number === playerPosition) return field ? { top: `${field.y}px`, left: `${field.x}px` } : { top: 0, left: 0 } }, [path]) // Get medal style - memoized const getMedalStyle = useCallback((rank) => { switch (rank) { case 1: return "bg-yellow-400 text-yellow-900 border-yellow-500 shadow-yellow-600" case 2: return "bg-gray-400 text-gray-900 border-gray-500 shadow-gray-600" case 3: return "bg-orange-500 text-orange-900 border-orange-600 shadow-orange-700" default: return "bg-gray-700 text-gray-300 border-gray-600 shadow-gray-800" } }, []) return (
{/* Connection Status Indicator */}
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
{error && !error.includes('Game not found') && !error.includes('token invalid') && (
⚠️ {error}
)}
{/* Game Info Bar */} {gameState && (
🎮 Játék kód: {gameState.gameCode || 'N/A'}
{currentTurn && (
🎯 Köron: {players.find(p => p.id === currentTurn)?.name || 'Betöltés...'}
)}
)}
{/* Game Board */}
{/* Background decoration */}
{[...Array(35)].map((_, i) => (
))}
{/* Fields */} {path.map((field) => (
{field.number}
))} {/* Player tokens */} {players.map((player) => (
{player.emoji}
))}
{/* Right sidebar */}

Játékosok

{/* Empty state */} {players.length === 0 && (
👥

Várakozás játékosokra...

)} {/* Players list */} {sortedPlayers.map((player, index) => (
{/* Online indicator */} {player.isOnline && (
)}
{player.emoji}
{player.name} {/* Ready indicator */} {player.isReady && ( ✓ Kész )} {/* Current turn indicator */} {currentTurn === player.id && ( ▶ Köre )} {/* Rank medal */} {index + 1 === 1 ? "🥇 1st" : index + 1 === 2 ? "🥈 2nd" : index + 1 === 3 ? "🥉 3rd" : `${index + 1}th`}
Pozíció: {player.position} • Pontszám: {player.score}
))}
{/* Dice Container */}

Dobókocka

Kattints a kockára dobáshoz!

{/* Connection warning */} {!isConnected && (
⚠️ Nincs kapcsolat a szerverrel
)}
{/* Debug Info Panel (Development only) */} {import.meta.env.DEV && (

🔧 Debug Info

📡 Connected: {isConnected ? '✅' : '❌'}
🎮 Game Code: {gameState?.gameCode || 'N/A'}
👥 Players: {backendPlayers?.length || 0}
🎲 Board Fields: {boardData?.fields?.length || 0}
🏁 Current Turn: {currentTurn || 'N/A'}
{/*
🔑 Token: {gameToken ? '✅' : '❌'}
*/}
)}
{/* 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} />
) } export default GameScreen