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, playerPositions, // NEW: Get dedicated position tracking state boardData: websocketBoardData, currentTurn, currentTurnName, isMyTurn, playerIdentifier, isGamemaster, error, playerDiceRolls, rollDice, approveJoker, rejectJoker, submitAnswer, submitPositionGuess, submitJokerPositionGuess, leaveGame, 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) const [isMyCardTurn, setIsMyCardTurn] = useState(false) // Track if I'm the one answering // 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) // End game modal state const [isEndGameModalOpen, setIsEndGameModalOpen] = useState(false) const [endGameData, setEndGameData] = useState(null) // Animation state management const [animatingPlayers, setAnimatingPlayers] = useState({}) // { playerId: { from, to, startTime, duration } } const [animatedPositions, setAnimatedPositions] = useState({}) // { playerId: currentAnimatedPosition } // 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 (UI properties only, no position) useEffect(() => { if (!backendPlayers?.length) return const mappedPlayers = backendPlayers.map((player, index) => { const playerName = player.playerName || player.name || `Player ${index + 1}`; return { id: player.playerId || player.id || index, name: playerName, // NO position stored here - always read from context playerPositions 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]) // Debug: Log playerPositions changes useEffect(() => { console.log('🔍 [GameScreen] playerPositions changed:', JSON.stringify(playerPositions)); players.forEach(p => { const pos = playerPositions?.[p.name]; console.log(`🔍 [GameScreen] Player ${p.name} position from context: ${pos}`); }); }, [playerPositions, players]); // Animation loop using requestAnimationFrame useEffect(() => { let animationFrameId const animate = () => { const now = Date.now() const updates = {} let hasActiveAnimations = false Object.entries(animatingPlayers).forEach(([playerId, animation]) => { const elapsed = now - animation.startTime const progress = Math.min(elapsed / animation.duration, 1) // Easing function (ease-in-out) const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2 // Interpolate position const currentPos = Math.round( animation.from + (animation.to - animation.from) * eased ) updates[playerId] = currentPos if (progress < 1) { hasActiveAnimations = true } // Animation complete - no local position update needed // Position always comes from context playerPositions }) if (Object.keys(updates).length > 0) { setAnimatedPositions(updates) } // Clean up completed animations if (!hasActiveAnimations && Object.keys(animatingPlayers).length > 0) { setAnimatingPlayers({}) setAnimatedPositions({}) } else if (hasActiveAnimations) { animationFrameId = requestAnimationFrame(animate) } } if (Object.keys(animatingPlayers).length > 0) { animationFrameId = requestAnimationFrame(animate) } return () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId) } } }, [animatingPlayers]) // Listen to player-moving event to start animation useEffect(() => { if (!addEventListener) return const handlePlayerMoving = (moveData) => { const duration = Math.abs(moveData.toPosition - moveData.fromPosition) * 50 // 50ms per position const clampedDuration = Math.max(500, Math.min(duration, 2000)) // Between 0.5s and 2s // Backend sends 1-based positions (1-100), use directly setAnimatingPlayers(prev => ({ ...prev, [moveData.playerId]: { from: moveData.fromPosition, to: moveData.toPosition, startTime: Date.now(), duration: clampedDuration } })) } addEventListener('game:player-moving', handlePlayerMoving) return () => removeEventListener('game:player-moving') }, [addEventListener, removeEventListener]) // Listen to errors and close modals useEffect(() => { if (!addEventListener) return const handleGameError = (errorData) => { console.error('❌ Game error:', errorData) // Close any open modals on error setIsCardModalOpen(false) setIsPredictionModalOpen(false) setIsJokerModalOpen(false) // Show error in consequence modal if severe if (errorData.message && errorData.message.includes('card')) { setCurrentConsequence({ isCorrect: false, explanation: errorData.message || 'An error occurred', consequenceType: null, consequenceValue: 0 }) setIsConsequenceModalOpen(true) } } addEventListener('game:error', handleGameError) return () => removeEventListener('game:error') }, [addEventListener, removeEventListener]) // Listen to player-arrived event (trigger animation for position changes) useEffect(() => { const handlePlayerArrived = (event) => { const arrivalData = event.detail // Context manager already updated playerPositions // Just set animation flag for visual animation const player = players.find(p => p.id === arrivalData.playerId || p.name === arrivalData.playerName) if (player) { setAnimatingPlayers(prevAnimating => ({ ...prevAnimating, [player.id]: true })) // Clear animation flag after animation completes (2 seconds) setTimeout(() => { setAnimatingPlayers(prevAnimating => { const newAnimating = { ...prevAnimating } delete newAnimating[player.id] return newAnimating }) }, 2000) } } // Listen to window CustomEvent instead of socket event (context already handles socket) window.addEventListener('game:player-arrived', handlePlayerArrived) return () => window.removeEventListener('game:player-arrived', handlePlayerArrived) }, [players]) // Listen to Joker card events useEffect(() => { if (!addEventListener) return const handleJokerDrawn = (jokerData) => { console.log('🃏 Joker kártya húzva:', jokerData) if (isGamemaster) { // Gamemaster sees approval modal with approve/deny buttons console.log('👑 Gamemaster látja a jóváhagyási modal-t') 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) } else { // Other players see the joker card as a read-only card display console.log('👥 Játékosok látják a joker kártyát (csak olvasás)') setCurrentCard({ type: 'JOKER', question: jokerData.jokerCard?.question || jokerData.cardTitle || 'Joker Kártya Feladat', consequence: jokerData.jokerCard?.consequence?.description || jokerData.cardDescription, playerName: jokerData.playerName, playerEmoji: jokerData.playerEmoji || "🎭", isReadOnly: true, waitingForGamemaster: true }) setIsCardModalOpen(true) setIsMyCardTurn(false) // Not my turn to answer } } // 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, isGamemaster]) // Listen to card drawn events (kártya megjelenítés) useEffect(() => { if (!addEventListener) return // Handle card drawn FOR ME (I need to answer) const handleCardDrawnSelf = (data) => { console.log('🎴 Kártya húzva NEKEM:', data) const cardData = data.cardData || data; console.log('📦 Card data structure:', cardData) setCurrentCard({ id: cardData.cardid || cardData.id, type: cardData.type, question: cardData.question || cardData.text || cardData.statement, answerOptions: cardData.answerOptions || [], sentencePairs: cardData.sentencePairs || [], words: cardData.words || [], acceptableAnswers: cardData.acceptableAnswers || [], correctAnswer: cardData.correctAnswer, hint: cardData.hint, points: cardData.points || 0, timeLimit: data.timeLimit || cardData.timeLimit || 60 }) setIsMyCardTurn(true) // I need to answer setIsCardModalOpen(true) } // Handle card drawn by ANOTHER PLAYER (spectator mode) const handleCardDrawn = (data) => { console.log('👀 Kártya húzva más játékos által:', data) const cardData = data.cardData || data; setCurrentCard({ id: cardData.cardid || cardData.id, type: cardData.type, question: cardData.question || cardData.text || cardData.statement, answerOptions: cardData.answerOptions || [], sentencePairs: cardData.sentencePairs || [], words: cardData.words || [], timeLimit: data.timeLimit || cardData.timeLimit || 60 }) setIsMyCardTurn(false) // Spectator mode setIsCardModalOpen(true) } addEventListener('game:card-drawn-self', handleCardDrawnSelf) addEventListener('game:card-drawn', handleCardDrawn) return () => { removeEventListener('game:card-drawn-self') removeEventListener('game:card-drawn') } }, [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) // Close card modal if it's open (shouldn't be for luck, but just in case) setIsCardModalOpen(false) setCurrentConsequence({ isCorrect: true, // Luck cards don't have right/wrong answers consequenceType: luckData.consequenceType, consequenceValue: luckData.value || luckData.consequenceValue || 0, explanation: luckData.description || luckData.message || 'Szerencse kártya!', playerAnswer: null, correctAnswer: null }) setIsConsequenceModalOpen(true) } const handleCardResult = (resultData) => { console.log('🎴 Card result (luck):', resultData) // This is for luck cards that use game:card-result event setIsCardModalOpen(false) setCurrentConsequence({ isCorrect: true, consequenceType: resultData.consequence?.type, consequenceValue: resultData.consequence?.value || 0, explanation: resultData.description || 'Szerencse kártya!', playerAnswer: null, correctAnswer: null }) setIsConsequenceModalOpen(true) } addEventListener('game:answer-validated', handleAnswerValidated) addEventListener('game:luck-consequence', handleLuckConsequence) addEventListener('game:card-result', handleCardResult) return () => { removeEventListener('game:answer-validated') removeEventListener('game:luck-consequence') removeEventListener('game:card-result') } }, [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) } const handleJokerPositionGuessRequest = (predictionData) => { console.log('🃏🎯 Joker után 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.message || 'Tippeld meg a végső pozíciódat joker után!', timeLimit: predictionData.timeLimit || 30, isJoker: true // Mark this as joker guess }) setIsPredictionModalOpen(true) } const handleGuessResult = (resultData) => { console.log('✅ Tippelés eredménye:', resultData) // Close prediction modal setIsPredictionModalOpen(false) // Backend already emits game:player-arrived before this event // Position is handled by context manager, no need for pendingPositionUpdate setCurrentConsequence({ isCorrect: resultData.guessCorrect, playerAnswer: resultData.guessedPosition, correctAnswer: resultData.actualPosition, explanation: resultData.message, consequenceType: resultData.penaltyApplied ? 'penalty' : 'success', consequenceValue: resultData.penaltyApplied ? -2 : 0 }) setIsConsequenceModalOpen(true) } const handleJokerComplete = (resultData) => { console.log('🃏✅ Joker tippelés eredménye:', resultData) // Close prediction modal setIsPredictionModalOpen(false) // Backend already emits game:player-arrived before this event (if moved) // Position is handled by context manager, no need for pendingPositionUpdate setCurrentConsequence({ isCorrect: resultData.guessCorrect, playerAnswer: resultData.guessedPosition, correctAnswer: resultData.actualPosition, explanation: resultData.message, consequenceType: resultData.penaltyApplied ? 'penalty' : (resultData.moved ? 'success' : 'neutral'), consequenceValue: resultData.penaltyApplied ? -2 : 0 }) setIsConsequenceModalOpen(true) } addEventListener('game:position-guess-request', handlePositionGuessRequest) addEventListener('game:joker-position-guess-request', handleJokerPositionGuessRequest) addEventListener('game:guess-result', handleGuessResult) addEventListener('game:joker-complete', handleJokerComplete) return () => { removeEventListener('game:position-guess-request') removeEventListener('game:joker-position-guess-request') removeEventListener('game:guess-result') removeEventListener('game:joker-complete') } }, [addEventListener, removeEventListener]) // Listen to game end event useEffect(() => { if (!addEventListener) return const handleGameEnded = (endData) => { console.log('🏆 Játék vége:', endData) setEndGameData({ winnerName: endData.winnerName, winnerId: endData.winner, finalPositions: endData.finalPositions || [], message: endData.message }) setIsEndGameModalOpen(true) } const handleGamemasterDecision = (decisionData) => { console.log('👑 Gamemaster döntés:', decisionData) // Close joker card modal for non-gamemaster players when decision is made if (!isGamemaster) { setIsCardModalOpen(false) } } addEventListener('game:ended', handleGameEnded) addEventListener('game:gamemaster-decision-result', handleGamemasterDecision) return () => { removeEventListener('game:ended') removeEventListener('game:gamemaster-decision-result') } }, [addEventListener, removeEventListener, isGamemaster]) // Joker jóváhagyás const handleApproveJoker = useCallback(async (jokerRequest) => { console.log('✅ Joker feladat jóváhagyva:', jokerRequest) // WebSocket üzenet a backend felé - csak requestId kell approveJoker(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é - csak requestId kell rejectJoker(jokerRequest.requestId, 'Joker rejected by gamemaster') // 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, 'Card ID:', currentCard?.id) // WebSocket emit a backend felé - uses context method with card ID submitAnswer(answer, currentCard?.id) // A consequence modal automatikusan megnyílik a 'game:answer-validated' event hatására }, [submitAnswer, currentCard]) // Consequence modal bezárása const handleConsequenceClose = useCallback(() => { // Position updates are handled by game:player-arrived event in context // No need to manually update positions here setIsConsequenceModalOpen(false) }, []) // 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é (különböző event joker-nél) if (currentPredictionData?.isJoker) { console.log('🃏 Joker pozíció tipp beküldése') submitJokerPositionGuess(predictedPosition) } else { submitPositionGuess(predictedPosition) } // Modal bezárása setIsPredictionModalOpen(false) }, [submitPositionGuess, submitJokerPositionGuess, currentPredictionData]) // Sorted players - memoized (sort by context position) const sortedPlayers = useMemo( () => [...players].sort((a, b) => { const posA = playerPositions?.[a.name] || 0 const posB = playerPositions?.[b.name] || 0 return posB - posA }), [players, playerPositions] ) // 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 (
Várakozás játékosokra...
🎯 A te köröd! Kattints a kockára dobáshoz!
⏳ Várd meg a köröd...
{endGameData.winnerName}
🎉 Nyert! 🎉