Files
SerpentRace/SerpentRace_Frontend/src/pages/Game/GameScreen.jsx
T
2025-11-24 23:28:57 +01:00

1087 lines
42 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className="p-4 bg-gradient-to-br from-gray-900 via-gray-800 to-teal-900 min-h-screen flex items-center justify-center">
<div className="w-full">
{/* Exit Game Button - Top Right Corner */}
<div className="fixed top-4 right-4 z-50">
<button
onClick={() => {
if (window.confirm('Biztosan ki szeretnél lépni a játékból?')) {
leaveGame()
window.location.href = '/'
}
}}
className="bg-red-600 hover:bg-red-700 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transition-colors duration-200 flex items-center gap-2 cursor-pointer"
title="Kilépés a játékból"
>
🚪 Kilépés
</button>
</div>
{/* Game Info Bar */}
{gameState && (
<div className="fixed top-4 left-4 z-50">
<div className="bg-gray-800 border border-teal-700 px-4 py-2 rounded-lg shadow-lg">
<div className="text-teal-300 text-sm font-medium">
🎮 Játék kód: <span className="font-bold text-white">{gameState.gameCode || 'N/A'}</span>
</div>
<div className="flex items-center gap-2 mt-1">
<div className={`w-2 h-2 rounded-full ${
isConnected ? 'bg-green-400 animate-pulse' : 'bg-red-400'
}`}></div>
<span className={`text-xs ${
isConnected ? 'text-green-400' : 'text-red-400'
}`}>
{isConnected ? 'Csatlakozva' : 'Kapcsolódás...'}
</span>
</div>
{currentTurn && (
<div className="text-gray-400 text-xs mt-1">
🎯 Köron: <span className={`font-bold ${isMyTurn ? 'text-green-400' : 'text-white'}`}>
{currentTurnName || players.find(p => p.id === currentTurn || p.playerName === currentTurn || p.name === currentTurn)?.name || currentTurn || 'Betöltés...'}
</span>
{isMyTurn && <span className="ml-2 text-green-400 animate-pulse"> Te vagy!</span>}
</div>
)}
</div>
</div>
)}
<div className="flex flex-col md:flex-row gap-6 justify-center">
{/* Game Board */}
<div className="relative bg-gray-800 p-6 rounded-2xl shadow-xl border border-teal-700 flex flex-col items-center justify-center overflow-hidden">
{/* Background decoration */}
<div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden">
{[...Array(35)].map((_, i) => (
<div
key={i}
className="absolute rounded-full bg-teal-600 animate-pulse"
style={{
width: Math.random() * 120 + 40 + "px",
height: Math.random() * 120 + 40 + "px",
top: Math.random() * 100 + "%",
left: Math.random() * 100 + "%",
transform: "translate(-50%, -50%)",
}}
></div>
))}
</div>
<div className="relative" style={{ height: `${height}px`, width: `${width}px` }}>
{/* Fields */}
{path.map((field) => (
<div
key={field.number}
className={`absolute w-10 h-10 border-2 flex items-center justify-center transition-all shadow-md hover:scale-110 ${getFieldStyle(
field.type
)}`}
style={{
top: `${field.y}px`,
left: `${field.x}px`,
width: `${cellSize}px`,
height: `${cellSize}px`,
margin: `${cellMargin}px`,
}}
>
<span className="text-gray-300 text-sm font-bold">{field.number}</span>
</div>
))}
{/* Player tokens */}
{players.map((player) => {
// ALWAYS read position from context playerPositions, not local state
// Backend uses 1-based indexing (1-100)
const contextPosition = playerPositions?.[player.name] ?? 1
// Use animated position if player is currently animating, otherwise use context position
const displayPosition = animatedPositions[player.id] ?? contextPosition
const isAnimating = animatingPlayers[player.id] !== undefined
return (
<div
key={player.id}
className={`absolute w-6 h-6 ${player.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-bold z-10 transition-transform ${isAnimating ? 'scale-110' : ''}`}
style={{
...getPlayerPosition(displayPosition),
transform: "translate(17px, 17px)",
transition: isAnimating ? 'none' : 'all 0.3s ease'
}}
>
{player.emoji}
</div>
)
})}
</div>
</div>
{/* Right sidebar */}
<div className="flex-1 max-w-md">
<div className="bg-gray-800 rounded-xl p-4 shadow-lg mb-4 border border-teal-700">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Játékosok</h2>
{/* Empty state */}
{players.length === 0 && (
<div className="text-center py-8 text-gray-400">
<div className="text-4xl mb-2">👥</div>
<p className="text-sm">Várakozás játékosokra...</p>
</div>
)}
{/* Players list */}
{sortedPlayers.map((player, index) => (
<div
key={player.id}
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors relative"
>
{/* Online indicator */}
{player.isOnline && (
<div className="absolute top-1 right-1 w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
)}
<div
className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`}
>
{player.emoji}
</div>
<div className="flex-1">
<div className="font-medium text-sm text-gray-300 flex items-center gap-2 flex-wrap">
{player.name}
{/* Ready indicator */}
{player.isReady && (
<span className="px-2 py-0.5 bg-green-600 text-white text-xs rounded-full">
Kész
</span>
)}
{/* Current turn indicator */}
{currentTurn === player.id && (
<span className="px-2 py-0.5 bg-yellow-500 text-gray-900 text-xs rounded-full font-bold animate-pulse">
Köre
</span>
)}
{/* Rank medal */}
<span
className={`ml-auto px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle(
index + 1
)}`}
>
{index + 1 === 1
? "🥇 1st"
: index + 1 === 2
? "🥈 2nd"
: index + 1 === 3
? "🥉 3rd"
: `${index + 1}th`}
</span>
</div>
<div className="text-xs text-gray-500">
Pozíció: {playerPositions?.[player.name] ?? 1} Pontszám: {player.score}
</div>
</div>
</div>
))}
</div>
{/* Dice Container */}
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
{isMyTurn ? (
<>
<p className="text-green-400 text-sm mb-4 font-bold animate-pulse">
🎯 A te köröd! Kattints a kockára dobáshoz!
</p>
<Dice onRoll={handleDiceRoll} />
</>
) : (
<>
<p className="text-gray-500 text-sm mb-4">
Várd meg a köröd...
</p>
<div className="opacity-50 pointer-events-none">
<Dice onRoll={handleDiceRoll} />
</div>
</>
)}
{/* Connection warning */}
{!isConnected && (
<div className="mt-3 text-xs text-red-400">
Nincs kapcsolat a szerverrel
</div>
)}
</div>
{/* Debug Info Panel (Development only) */}
{import.meta.env.DEV && (
<div className="bg-gray-900 rounded-xl p-4 shadow-lg border border-gray-700 text-left mt-4">
<h3 className="text-sm font-semibold mb-2 text-gray-400">🔧 Debug Info</h3>
<div className="text-xs text-gray-500 space-y-1">
<div>📡 Connected: {isConnected ? '✅' : '❌'}</div>
<div>🎮 Game Code: {gameState?.gameCode || 'N/A'}</div>
<div>👥 Players: {backendPlayers?.length || 0}</div>
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
<div>🏁 Current Turn: {currentTurnName || currentTurn || 'N/A'}</div>
<div>🆔 My ID: {playerIdentifier || 'N/A'}</div>
<div> Is My Turn: {isMyTurn ? 'YES' : 'NO'}</div>
{/* <div>🔑 Token: {gameToken ? '✅' : '❌'}</div> */}
</div>
</div>
)}
</div>
</div>
</div>
{/* Joker Approval Modal - csak Gamemaster számára */}
<JokerApprovalModal
isOpen={isJokerModalOpen}
onClose={() => setIsJokerModalOpen(false)}
jokerRequest={currentJokerRequest}
onApprove={handleApproveJoker}
onReject={handleRejectJoker}
playerName={currentJokerRequest?.playerName}
playerEmoji={currentJokerRequest?.playerEmoji}
/>
{/* Card Display Modal - kártya megjelenítés */}
<CardDisplayModal
isOpen={isCardModalOpen}
onClose={() => setIsCardModalOpen(false)}
card={currentCard}
onSubmitAnswer={handleSubmitAnswer}
isMyTurn={isMyCardTurn}
/>
{/* Consequence Modal - következmények megjelenítése */}
<ConsequenceModal
isOpen={isConsequenceModalOpen}
onClose={handleConsequenceClose}
isCorrect={currentConsequence?.isCorrect}
consequenceType={currentConsequence?.consequenceType}
consequenceValue={currentConsequence?.consequenceValue}
playerAnswer={currentConsequence?.playerAnswer}
correctAnswer={currentConsequence?.correctAnswer}
explanation={currentConsequence?.explanation}
/>
{/* End Game Modal */}
{isEndGameModalOpen && endGameData && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90">
<div className="relative bg-gradient-to-br from-yellow-900 via-yellow-800 to-yellow-900 rounded-2xl shadow-2xl border-4 border-yellow-500 max-w-2xl w-full p-8 text-center">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
<div className="relative">
<div className="text-8xl mb-4 animate-bounce">🏆</div>
<h1 className="text-5xl font-bold text-white mb-4">
Játék vége!
</h1>
<div className="bg-black/30 rounded-xl p-6 mb-6">
<p className="text-6xl font-bold text-yellow-300 mb-2">
{endGameData.winnerName}
</p>
<p className="text-2xl text-yellow-100">
🎉 Nyert! 🎉
</p>
</div>
{endGameData.finalPositions && endGameData.finalPositions.length > 0 && (
<div className="bg-black/30 rounded-xl p-4 mb-6">
<h3 className="text-xl font-semibold text-yellow-300 mb-3">Végső állás:</h3>
<div className="space-y-2">
{endGameData.finalPositions
.sort((a, b) => b.boardPosition - a.boardPosition)
.map((player, index) => (
<div key={player.playerId} className="flex items-center justify-between bg-yellow-900/30 rounded-lg p-3">
<div className="flex items-center gap-3">
<span className="text-2xl">
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎮'}
</span>
<span className="text-white font-semibold">{player.playerName}</span>
</div>
<span className="text-yellow-300 font-bold">Pozíció: {player.boardPosition}</span>
</div>
))}
</div>
</div>
)}
<button
onClick={() => window.location.href = '/'}
className="bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-500 hover:to-orange-500 text-white font-bold py-4 px-8 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-105 text-xl"
>
Vissza a főoldalra
</button>
</div>
</div>
</div>
)}
{/* Step Prediction Modal - pozíció tippelés */}
<StepPredictionModal
isOpen={isPredictionModalOpen}
onClose={() => setIsPredictionModalOpen(false)}
currentPosition={currentPredictionData?.currentPosition || 0}
diceRoll={currentPredictionData?.diceRoll || 0}
fieldStepValue={currentPredictionData?.fieldStepValue || 0}
patternModifier={currentPredictionData?.patternModifier || 0}
cardText={currentPredictionData?.cardText || "Tippeld meg, melyik pozícióra fogsz lépni!"}
hints={currentPredictionData?.hints || []}
timeLimit={currentPredictionData?.timeLimit || 30}
isJoker={currentPredictionData?.isJoker || false}
onSubmitPrediction={handleSubmitPrediction}
/>
</div>
)
}
export default GameScreen