328 lines
14 KiB
React
328 lines
14 KiB
React
import React, { useState, useEffect } from "react"
|
||
import { motion, AnimatePresence } from "framer-motion"
|
||
|
||
/**
|
||
* StepPredictionModal - Pozíció tippelés (Position Guessing)
|
||
*
|
||
* A dokumentáció szerint: A játékosnak meg kell tippelnie a VÉGLEGES POZÍCIÓT,
|
||
* nem a lépésszámot!
|
||
*
|
||
* Számítás: finalPosition = currentPosition + diceRoll + fieldStepValue + patternModifier
|
||
*
|
||
* @param {Object} props
|
||
* @param {boolean} props.isOpen - Modal megjelenítése
|
||
* @param {Function} props.onClose - Modal bezárása
|
||
* @param {Function} props.onSubmitPrediction - Tipp beküldése
|
||
* @param {number} props.currentPosition - Jelenlegi pozíció
|
||
* @param {number} props.diceRoll - Dobás értéke
|
||
* @param {number} props.fieldStepValue - Mező lépés értéke
|
||
* @param {number} props.patternModifier - Zóna alapú módosító
|
||
* @param {string} props.cardText - Kártya szövege
|
||
* @param {Array} props.hints - Segédletek tömbje
|
||
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 30)
|
||
*/
|
||
const StepPredictionModal = ({
|
||
isOpen,
|
||
onClose,
|
||
onSubmitPrediction,
|
||
currentPosition = 0,
|
||
diceRoll = 0,
|
||
fieldStepValue = 0,
|
||
patternModifier = 0,
|
||
cardText = "Tippeld meg, melyik pozícióra fogsz lépni!",
|
||
hints = [],
|
||
timeLimit = 30
|
||
}) => {
|
||
const [prediction, setPrediction] = useState("")
|
||
const [showHints, setShowHints] = useState(false)
|
||
const [isProcessing, setIsProcessing] = useState(false)
|
||
const [timeLeft, setTimeLeft] = useState(timeLimit)
|
||
|
||
// Timer countdown
|
||
useEffect(() => {
|
||
if (!isOpen) return
|
||
|
||
// Reset time when modal opens
|
||
setTimeLeft(timeLimit)
|
||
|
||
const timer = setInterval(() => {
|
||
setTimeLeft(prev => {
|
||
if (prev <= 1) {
|
||
clearInterval(timer)
|
||
handleTimeout()
|
||
return 0
|
||
}
|
||
return prev - 1
|
||
})
|
||
}, 1000)
|
||
|
||
return () => clearInterval(timer)
|
||
}, [isOpen]) // Remove timeLimit from dependencies to prevent timer reset
|
||
|
||
const handleTimeout = () => {
|
||
if (onSubmitPrediction && !isProcessing) {
|
||
onSubmitPrediction(null) // null = timeout
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
if (!prediction || prediction === "" || isProcessing) return
|
||
|
||
const guessedPosition = parseInt(prediction)
|
||
if (isNaN(guessedPosition)) return
|
||
|
||
setIsProcessing(true)
|
||
|
||
try {
|
||
await onSubmitPrediction(guessedPosition)
|
||
} catch (error) {
|
||
console.error("Tipp küldési hiba:", error)
|
||
setIsProcessing(false)
|
||
}
|
||
}
|
||
|
||
// Reset amikor megnyílik
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
setPrediction("")
|
||
setShowHints(false)
|
||
setIsProcessing(false)
|
||
}
|
||
}, [isOpen])
|
||
|
||
// Számított végső pozíció (helyes válasz)
|
||
// Backend formula: currentPosition + (stepValue × dice) + patternModifier
|
||
const movement = fieldStepValue * diceRoll
|
||
const calculatedPosition = currentPosition + movement + patternModifier
|
||
|
||
const formatTime = (seconds) => {
|
||
return `0:${seconds.toString().padStart(2, '0')}`
|
||
}
|
||
|
||
const getTimeColor = () => {
|
||
if (timeLeft > 15) return "text-green-400"
|
||
if (timeLeft > 5) return "text-yellow-400"
|
||
return "text-red-400 animate-pulse"
|
||
}
|
||
|
||
if (!isOpen) return null
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
{/* Backdrop */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||
/>
|
||
|
||
{/* Modal Content */}
|
||
<motion.div
|
||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||
transition={{ type: "spring", duration: 0.5 }}
|
||
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-yellow-500/30 max-w-xl w-full overflow-hidden max-h-[90vh] overflow-y-auto"
|
||
>
|
||
{/* Header */}
|
||
<div className="bg-gradient-to-r from-yellow-600 via-orange-600 to-yellow-600 p-4 relative overflow-hidden sticky top-0 z-10">
|
||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
||
<div className="relative flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<div className="text-3xl animate-bounce">🎯</div>
|
||
<div>
|
||
<h2 className="text-xl font-bold text-white">Pozíció Tippelés</h2>
|
||
<p className="text-white/80 text-xs">Melyik pozícióra lépsz?</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Timer */}
|
||
<div className="bg-black/30 rounded-lg px-3 py-1">
|
||
<div className={`text-lg font-bold ${getTimeColor()}`}>
|
||
⏱️ {formatTime(timeLeft)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="p-4 space-y-4">
|
||
{/* Card Text / Instructions */}
|
||
<div className="bg-gray-800/50 rounded-xl p-3 border border-gray-700">
|
||
<div className="flex items-start gap-2">
|
||
<div className="text-2xl">📝</div>
|
||
<div className="flex-1">
|
||
<p className="text-white text-sm leading-relaxed">
|
||
{cardText}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step-by-Step Calculation Helper */}
|
||
<div className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 rounded-xl p-4 border border-blue-500/30">
|
||
<h3 className="text-blue-300 font-semibold mb-3 text-center">🧮 Számítási Adatok</h3>
|
||
|
||
{/* Visual calculation steps */}
|
||
<div className="space-y-2 mb-3">
|
||
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
|
||
<span className="text-gray-300">Kezdő pozíció:</span>
|
||
<span className="text-white font-bold text-xl">{currentPosition}</span>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
|
||
<span className="text-gray-300">Mező érték:</span>
|
||
<span className="text-yellow-400 font-bold text-xl">{fieldStepValue}</span>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
|
||
<span className="text-gray-300">Dobás:</span>
|
||
<span className="text-blue-400 font-bold text-xl">{diceRoll}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pattern Modifier Info Box */}
|
||
<div className="bg-indigo-900/30 rounded-lg p-4 border border-indigo-500/30">
|
||
<div className="flex items-start gap-2 mb-2">
|
||
<span className="text-2xl">ℹ️</span>
|
||
<div className="flex-1">
|
||
<h4 className="text-indigo-300 font-semibold mb-1">Zóna módosító szabályok:</h4>
|
||
<ul className="text-gray-300 text-sm space-y-1">
|
||
<li>• <strong>0-ra végződik</strong> (10, 20...): nincs módosító</li>
|
||
<li>• <strong>5-re végződik</strong> (15, 25...): ±3 módosító</li>
|
||
<li>• <strong>3-mal osztható</strong> (9, 12, 21...): ±2 módosító</li>
|
||
<li>• <strong>Páratlan</strong> (1, 7, 11...): ±1 módosító</li>
|
||
<li>• <strong>Egyéb páros</strong>: nincs módosító</li>
|
||
</ul>
|
||
<p className="text-indigo-200 text-xs mt-2">A módosító előjele a mező típusától függ (+ pozitív, - negatív mező)</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Formula hint without answer */}
|
||
<div className="bg-yellow-900/20 rounded-lg p-3 border border-yellow-500/30 mt-3">
|
||
<p className="text-yellow-200 text-xs text-center">
|
||
💡 <strong>Képlet:</strong> Kezdő + (Mező × Dobás) + Zóna módosító
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Position Input */}
|
||
<div className="space-y-2">
|
||
<h3 className="text-yellow-300 font-semibold text-center">
|
||
✍️ Írd be a tippelt pozíciót:
|
||
</h3>
|
||
<input
|
||
type="number"
|
||
value={prediction}
|
||
onChange={(e) => setPrediction(e.target.value)}
|
||
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
|
||
disabled={isProcessing}
|
||
placeholder={`Pl.: ${Math.floor(currentPosition + diceRoll * 1.5)}`}
|
||
className="w-full bg-gray-800 border-2 border-yellow-600 rounded-xl px-4 py-3 text-white text-center text-2xl font-bold focus:border-yellow-400 focus:outline-none disabled:opacity-50"
|
||
min={0}
|
||
max={100}
|
||
/>
|
||
</div>
|
||
|
||
{/* Show entered prediction */}
|
||
{prediction && prediction !== "" && (
|
||
<motion.div
|
||
initial={{ scale: 0.8, opacity: 0 }}
|
||
animate={{ scale: 1, opacity: 1 }}
|
||
className="bg-yellow-900/20 rounded-xl p-3 border border-yellow-500/30 text-center"
|
||
>
|
||
<p className="text-yellow-300 text-sm">
|
||
A tipped: <span className="font-bold text-xl text-white">{prediction}</span> pozíció
|
||
</p>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* Hints Section */}
|
||
{hints && hints.length > 0 && (
|
||
<div className="bg-blue-900/20 rounded-xl p-4 border border-blue-500/30">
|
||
<button
|
||
onClick={() => setShowHints(!showHints)}
|
||
className="w-full flex items-center justify-between text-blue-300 hover:text-blue-200 transition-colors"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-2xl">💡</span>
|
||
<span className="font-semibold">Segédlet</span>
|
||
</div>
|
||
<span className="text-xl">{showHints ? "▼" : "▶"}</span>
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{showHints && (
|
||
<motion.div
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: "auto", opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
className="mt-3 space-y-2"
|
||
>
|
||
{hints.map((hint, index) => (
|
||
<div key={index} className="bg-blue-900/30 rounded-lg p-3">
|
||
<p className="text-gray-300 text-sm">
|
||
<span className="font-bold text-blue-300">#{index + 1}:</span> {hint}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)}
|
||
|
||
{/* Risk/Reward Info */}
|
||
<div className="bg-gradient-to-br from-green-900/20 to-red-900/20 rounded-xl p-4 border border-gray-600">
|
||
<div className="grid grid-cols-2 gap-4 text-center">
|
||
<div>
|
||
<div className="text-3xl mb-2">✅</div>
|
||
<p className="text-green-300 font-semibold text-sm">Ha eltalálod</p>
|
||
<p className="text-white text-xs">Lépsz az új pozícióra!</p>
|
||
</div>
|
||
<div>
|
||
<div className="text-3xl mb-2">❌</div>
|
||
<p className="text-red-300 font-semibold text-sm">Ha nem találod el</p>
|
||
<p className="text-white text-xs">-2 büntetés + nem lépsz!</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Submit Button */}
|
||
<button
|
||
onClick={handleSubmit}
|
||
disabled={!prediction || prediction === "" || isProcessing}
|
||
className="w-full 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-6 rounded-xl shadow-lg
|
||
transform transition-all duration-200 hover:scale-105 active:scale-95
|
||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||
border border-yellow-500/50"
|
||
>
|
||
<div className="flex items-center justify-center gap-2">
|
||
<span className="text-2xl">🎲</span>
|
||
<span className="text-lg">
|
||
{isProcessing ? "Feldolgozás..." : "Tipp beküldése"}
|
||
</span>
|
||
</div>
|
||
</button>
|
||
|
||
{/* Warning */}
|
||
{(!prediction || prediction === "") && (
|
||
<p className="text-center text-gray-400 text-sm">
|
||
⚠️ Írd be a tippelt pozíciót a beküldéshez!
|
||
</p>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
)
|
||
}
|
||
|
||
export default StepPredictionModal
|