674 lines
25 KiB
React
674 lines
25 KiB
React
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 (
|
||
<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">
|
||
{/* Connection Status Indicator */}
|
||
<div className="fixed top-4 right-4 z-50">
|
||
<div className={`px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 ${
|
||
isConnected
|
||
? 'bg-green-600 text-white'
|
||
: 'bg-red-600 text-white'
|
||
}`}>
|
||
<div className={`w-3 h-3 rounded-full ${
|
||
isConnected ? 'bg-green-300 animate-pulse' : 'bg-red-300'
|
||
}`}></div>
|
||
<span className="text-sm font-medium">
|
||
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
|
||
</span>
|
||
</div>
|
||
{error && !error.includes('Game not found') && !error.includes('token invalid') && (
|
||
<div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
|
||
⚠️ {error}
|
||
</div>
|
||
)}
|
||
</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>
|
||
{currentTurn && (
|
||
<div className="text-gray-400 text-xs mt-1">
|
||
🎯 Köron: <span className="text-white">{players.find(p => p.id === currentTurn)?.name || 'Betöltés...'}</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) => (
|
||
<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 animate-bounce`}
|
||
style={{
|
||
...getPlayerPosition(player.position),
|
||
transform: "translate(17px, 17px)",
|
||
}}
|
||
>
|
||
{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ó: {player.position} • 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>
|
||
<p className="text-gray-300 text-sm mb-4">
|
||
Kattints a kockára dobáshoz!
|
||
</p>
|
||
|
||
<Dice onRoll={handleDiceRoll} />
|
||
|
||
{/* 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: {currentTurn || 'N/A'}</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}
|
||
/>
|
||
|
||
{/* Consequence Modal - következmények megjelenítése */}
|
||
<ConsequenceModal
|
||
isOpen={isConsequenceModalOpen}
|
||
onClose={() => setIsConsequenceModalOpen(false)}
|
||
consequence={currentConsequence}
|
||
/>
|
||
|
||
{/* Step Prediction Modal - pozíció tippelés */}
|
||
<StepPredictionModal
|
||
isOpen={isPredictionModalOpen}
|
||
onClose={() => setIsPredictionModalOpen(false)}
|
||
predictionData={currentPredictionData}
|
||
onSubmitPrediction={handleSubmitPrediction}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default GameScreen
|