csatlakozas-mukodesdemodemodemo
This commit is contained in:
@@ -1,88 +1,189 @@
|
||||
import React, { useState } from "react"
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { getVerticalOffset } from "../../utils/randomUtils"
|
||||
import Dice from "../../utils/dice/Dice"
|
||||
import { useGameWebSocket } from "../../hooks/useGameWebSocket"
|
||||
|
||||
// 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 = () => {
|
||||
const boardRows = 5
|
||||
const boardCols = 20
|
||||
const totalCells = boardRows * boardCols
|
||||
const cellSize = 40
|
||||
const cellMargin = 2.5
|
||||
const rowSpacing = 70 // Extra spacing between rows
|
||||
const topOffset = rowSpacing * 0.5 // Increase topOffset for more spacing
|
||||
const bottomOffset = rowSpacing * 0.5 // Increase bottomOffset for more spacing
|
||||
const boardWidthPx = boardCols * (cellSize + cellMargin * 2)
|
||||
const boardHeightPx =
|
||||
boardRows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing
|
||||
// WebSocket connection
|
||||
const gameToken = localStorage.getItem('gameToken')
|
||||
const {
|
||||
isConnected,
|
||||
gameState,
|
||||
players: backendPlayers,
|
||||
boardData,
|
||||
currentTurn,
|
||||
error,
|
||||
rollDice,
|
||||
addEventListener,
|
||||
removeEventListener
|
||||
} = useGameWebSocket(gameToken)
|
||||
|
||||
const [path, setPath] = useState([])
|
||||
const [players, setPlayers] = useState([])
|
||||
|
||||
// Generate a snake-like path with vertical spacing and vertical offsets
|
||||
const generateWindingPath = () => {
|
||||
// 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
|
||||
|
||||
for (let row = 0; row < boardRows && currentNum <= totalCells; row++) {
|
||||
// Calculate the y position with extra row spacing
|
||||
const baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing)
|
||||
|
||||
// If row number is even, go right; if odd, go left
|
||||
if (row % 2 === 0) {
|
||||
// Left to right
|
||||
for (let col = 0; col < boardCols && currentNum <= totalCells; col++) {
|
||||
path.push({
|
||||
number: currentNum++,
|
||||
x: col * (cellSize + cellMargin * 2),
|
||||
y: baseYPosition + getVerticalOffset(currentNum - 1),
|
||||
type: getFieldType(currentNum - 1),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Right to left
|
||||
for (let col = boardCols - 1; col >= 0 && currentNum <= totalCells; col--) {
|
||||
path.push({
|
||||
number: currentNum++,
|
||||
x: col * (cellSize + cellMargin * 2),
|
||||
y: baseYPosition + getVerticalOffset(currentNum - 1),
|
||||
type: getFieldType(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])
|
||||
|
||||
const getFieldType = (count) => {
|
||||
if (count % 17 === 0) return "clover"
|
||||
if (count % 13 === 0) return "bad"
|
||||
if ((count + 5) % 13 === 0) return "good"
|
||||
return "regular"
|
||||
}
|
||||
// Update path when boardData changes
|
||||
useEffect(() => {
|
||||
if (boardData?.fields) {
|
||||
setPath(generateWindingPath(boardData.fields))
|
||||
} else if (path.length === 0) {
|
||||
setPath(generateWindingPath())
|
||||
}
|
||||
}, [boardData, generateWindingPath])
|
||||
|
||||
const [path, setPath] = useState(generateWindingPath())
|
||||
const [players, setPlayers] = useState([
|
||||
{ id: 1, name: "Béla", position: 34, score: 25, color: "bg-blue-600", emoji: "🐍" },
|
||||
{ id: 2, name: "Juci", position: 50, score: 30, color: "bg-green-600", emoji: "🐢" },
|
||||
{ id: 3, name: "Kati", position: 70, score: 15, color: "bg-purple-600", emoji: "🐇" },
|
||||
{ id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" },
|
||||
])
|
||||
// Update players from backend - memoized mapping
|
||||
useEffect(() => {
|
||||
if (!backendPlayers?.length) return
|
||||
|
||||
// New: selected dice value from dropdown (null = none)
|
||||
const [selectedDice, setSelectedDice] = useState(null)
|
||||
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])
|
||||
|
||||
// Sort players by position in descending order
|
||||
const sortedPlayers = [...players].sort((a, b) => b.position - a.position)
|
||||
// Listen to player movement - optimized to update only moved player
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
// Handle dice roll completion
|
||||
const handleDiceRoll = (value) => {
|
||||
console.log("Rolled:", value)
|
||||
// reset dropdown selection after roll
|
||||
setSelectedDice(null)
|
||||
// You can add logic here to move the current player based on the dice value
|
||||
}
|
||||
const handlePlayerMoved = (moveData) => {
|
||||
setPlayers(prev =>
|
||||
prev.map(p =>
|
||||
p.id === moveData.playerId
|
||||
? { ...p, position: moveData.newPosition }
|
||||
: p
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
console.log("Generated path length:", path.length)
|
||||
addEventListener('game:player-moved', handlePlayerMoved)
|
||||
return () => removeEventListener('game:player-moved')
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
const getFieldStyle = (type) => {
|
||||
// Sorted players - memoized
|
||||
const sortedPlayers = useMemo(
|
||||
() => [...players].sort((a, b) => b.position - a.position),
|
||||
[players]
|
||||
)
|
||||
|
||||
// Handle dice roll
|
||||
const handleDiceRoll = useCallback((value) => {
|
||||
rollDice(value)
|
||||
}, [rollDice])
|
||||
|
||||
// Get field style - memoized
|
||||
const getFieldStyle = useCallback((type) => {
|
||||
switch (type) {
|
||||
case "clover":
|
||||
return "bg-teal-700 border-teal-500 shadow-teal-800"
|
||||
@@ -93,15 +194,16 @@ const GameScreen = () => {
|
||||
default:
|
||||
return "bg-gray-800 border-gray-600 shadow-gray-900"
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getPlayerPosition = (playerPosition) => {
|
||||
// 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])
|
||||
|
||||
// Function to get medal style based on rank
|
||||
const getMedalStyle = (rank) => {
|
||||
// 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"
|
||||
@@ -112,20 +214,57 @@ const GameScreen = () => {
|
||||
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 && (
|
||||
<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">
|
||||
{/* Háttér */}
|
||||
{/* 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-pulse8"
|
||||
className="absolute rounded-full bg-teal-600 animate-pulse"
|
||||
style={{
|
||||
width: Math.random() * 120 + 40 + "px",
|
||||
height: Math.random() * 120 + 40 + "px",
|
||||
@@ -136,8 +275,9 @@ const GameScreen = () => {
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative" style={{ height: `${boardHeightPx}px`, width: `${boardWidthPx}px` }}>
|
||||
{/* Mezők */}
|
||||
|
||||
<div className="relative" style={{ height: `${height}px`, width: `${width}px` }}>
|
||||
{/* Fields */}
|
||||
{path.map((field) => (
|
||||
<div
|
||||
key={field.number}
|
||||
@@ -163,44 +303,65 @@ const GameScreen = () => {
|
||||
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(18px, 18px)",
|
||||
transform: "translate(17px, 17px)",
|
||||
}}
|
||||
>
|
||||
{player.emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Game information */}
|
||||
{/* <div className="bg-white rounded-xl p-2 shadow-lg border border-indigo-100 max-w-3xl mx-auto mt-4 z-10">
|
||||
<p className="text-gray-600 text-sm text-center">
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-white border border-gray-300 rounded-full mr-1"></span> Sima</span>
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-green-200 border border-green-500 rounded-full mr-1"></span> Lóhere</span>
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-red-200 border border-red-500 rounded-full mr-1"></span> Rossz</span>
|
||||
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-blue-200 border border-blue-500 rounded-full mr-1"></span> Jó</span>
|
||||
</p>
|
||||
</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"
|
||||
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">
|
||||
<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-2 px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle(
|
||||
className={`ml-auto px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle(
|
||||
index + 1
|
||||
)}`}
|
||||
>
|
||||
@@ -225,31 +386,33 @@ const GameScreen = () => {
|
||||
<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 vagy válassz egy számot az alábbiból!
|
||||
Kattints a kockára dobáshoz!
|
||||
</p>
|
||||
|
||||
{/* Dropdown to select number 1-6 (triggers animated roll to that number) */}
|
||||
<div className="mb-3">
|
||||
<select
|
||||
value={selectedDice ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value ? Number(e.target.value) : null
|
||||
setSelectedDice(v)
|
||||
}}
|
||||
className="bg-gray-900 text-gray-200 rounded-md p-2 border border-gray-700"
|
||||
>
|
||||
<option value="">Válassz számot...</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} />
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user