feat: Mejoras UI - Timer, mapa resultado misión y tokens
- Mover timer de votación a esquina superior izquierda (fixed, 20px margen) - Eliminar contador de votos rechazados en resultado de misión - Ajustar posiciones de tokens de victoria/fracaso en el mapa - Mantener mapa visible durante toda la fase MISSION_RESULT (eliminar timeout de 7s) - Cambiar título intro de 'Guerra Total' a 'Traidores en París' - Ajustar tamaño de cartas aceptar/rechazar líder a cuadradas (w-32 h-32) TODO: Afinar posiciones de tokens 3, 4 y 5 en el mapa
|
Before Width: | Height: | Size: 764 KiB After Width: | Height: | Size: 985 KiB |
BIN
client/public/assets/images/missions/mission2-p2.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 720 KiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 680 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 716 KiB After Width: | Height: | Size: 920 KiB |
|
Before Width: | Height: | Size: 691 KiB After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 764 KiB |
|
After Width: | Height: | Size: 720 KiB |
|
After Width: | Height: | Size: 680 KiB |
|
After Width: | Height: | Size: 716 KiB |
|
After Width: | Height: | Size: 691 KiB |
BIN
client/public/assets/images/missions/quadrades/mission1.png
Normal file
|
After Width: | Height: | Size: 893 KiB |
BIN
client/public/assets/images/missions/quadrades/mission2.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
client/public/assets/images/missions/quadrades/mission2.png
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
client/public/assets/images/missions/quadrades/mission3.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
client/public/assets/images/missions/quadrades/mission4.png
Normal file
|
After Width: | Height: | Size: 875 KiB |
BIN
client/public/assets/images/missions/quadrades/mission5.jpg
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
client/public/assets/images/missions/quadrades/mission5.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
client/public/assets/images/missions/thumbdown.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
client/public/assets/images/missions/thumbup.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
client/public/assets/images/tokens/accept_leader.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.2 MiB |
BIN
client/public/assets/images/tokens/deny_leader.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.4 MiB |
BIN
client/public/assets/images/tokens/originals/accept_leader.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
client/public/assets/images/tokens/originals/deny_leader.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
@@ -100,6 +100,7 @@ export default function Home() {
|
||||
<div className="z-10 bg-black/80 p-8 rounded border border-white/20 max-w-2xl w-full mx-4 backdrop-blur-md">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-3xl font-bold text-yellow-500 mb-2 uppercase tracking-widest">Sala de Espera</h2>
|
||||
<p className="text-xl font-bold text-white mb-1">{gameState.roomName}</p>
|
||||
<p className="text-gray-400">Operación en curso. Esperando activación...</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,14 +53,10 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
// Estado para controlar cuándo mostrar el tablero
|
||||
const [showBoard, setShowBoard] = useState(false);
|
||||
|
||||
// Mostrar tablero 7 segundos después de MISSION_RESULT
|
||||
// Mostrar tablero durante MISSION_RESULT
|
||||
useEffect(() => {
|
||||
if (gameState.phase === GamePhase.MISSION_RESULT) {
|
||||
setShowBoard(true);
|
||||
const timer = setTimeout(() => {
|
||||
setShowBoard(false);
|
||||
}, 7000); // 7 segundos
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setShowBoard(false);
|
||||
}
|
||||
@@ -91,11 +87,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
// Coordenadas porcentuales de los hexágonos de misión en el mapa
|
||||
const missionCoords = [
|
||||
{ left: '12%', top: '55%' }, // Misión 1
|
||||
{ left: '28%', top: '15%' }, // Misión 2
|
||||
{ left: '52%', top: '25%' }, // Misión 3
|
||||
{ left: '42%', top: '70%' }, // Misión 4
|
||||
{ left: '82%', top: '40%' }, // Misión 5
|
||||
{ left: '18%', top: '60%' }, // Misión 1 - Abajo izquierda
|
||||
{ left: '25%', top: '18%' }, // Misión 2 - Arriba izquierda
|
||||
{ left: '50%', top: '75%' }, // Misión 3 - Abajo centro
|
||||
{ left: '50%', top: '30%' }, // Misión 4 - Centro
|
||||
{ left: '80%', top: '45%' }, // Misión 5 - Derecha
|
||||
];
|
||||
|
||||
// Nombres de las misiones
|
||||
@@ -120,7 +116,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
</div>
|
||||
|
||||
<h1 className="z-10 text-5xl font-bold uppercase tracking-[0.3em] mb-8 text-yellow-500 drop-shadow-lg text-center">
|
||||
Sombras en París
|
||||
Traidores en París
|
||||
</h1>
|
||||
|
||||
{/* Audio Auto-Play - Solo para el host */}
|
||||
@@ -295,8 +291,8 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-screen flex flex-col items-center overflow-hidden">
|
||||
|
||||
<div className="relative w-full h-screen flex flex-col overflow-hidden">
|
||||
{/* Fondo */}
|
||||
<div className="absolute inset-0 z-0 opacity-40">
|
||||
<Image
|
||||
src={
|
||||
@@ -313,7 +309,8 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
<div className="absolute inset-0 bg-black/60" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full flex flex-col items-center">
|
||||
{/* Contenedor principal */}
|
||||
<div className="relative z-10 w-full flex flex-col items-center pb-32">
|
||||
|
||||
{/* --- MAPA TÁCTICO (TABLERO) O CARTA DE MISIÓN O ASSASSIN_PHASE --- */}
|
||||
{/* No mostrar el tablero en fases de victoria */}
|
||||
@@ -415,16 +412,6 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* TRACK DE VOTOS FALLIDOS */}
|
||||
<div className="absolute bottom-[5%] left-[2%] bg-black/60 p-2 rounded border border-white/20">
|
||||
<div className="text-[10px] text-gray-300 uppercase mb-1 text-center">Votos Rechazados</div>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className={`w-3 h-3 rounded-full border border-gray-500 ${i < gameState.failedVotesCount ? 'bg-red-500' : 'bg-transparent'}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* CARTA DE MISIÓN CON TÍTULO */
|
||||
@@ -478,13 +465,13 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
{gameState.leaderVotes?.[currentPlayerId] === undefined ? (
|
||||
<div className="flex gap-8">
|
||||
<button onClick={() => actions.voteLeader(true)} className="group">
|
||||
<div className="w-40 h-60 bg-white rounded-lg shadow-xl flex items-center justify-center border-4 border-transparent group-hover:border-green-500 transition-all transform group-hover:-translate-y-4 relative overflow-hidden">
|
||||
<div className="w-32 h-32 bg-white rounded-lg shadow-xl flex items-center justify-center border-4 border-transparent group-hover:border-green-500 transition-all transform group-hover:-translate-y-4 relative overflow-hidden">
|
||||
<Image src="/assets/images/tokens/accept_leader.png" alt="Accept Leader" fill className="object-contain" />
|
||||
</div>
|
||||
<span className="block text-center text-white mt-2 font-bold bg-green-600 px-2 rounded uppercase tracking-widest">ACEPTAR</span>
|
||||
</button>
|
||||
<button onClick={() => actions.voteLeader(false)} className="group">
|
||||
<div className="w-40 h-60 bg-white rounded-lg shadow-xl flex items-center justify-center border-4 border-transparent group-hover:border-red-500 transition-all transform group-hover:-translate-y-4 relative overflow-hidden">
|
||||
<div className="w-32 h-32 bg-white rounded-lg shadow-xl flex items-center justify-center border-4 border-transparent group-hover:border-red-500 transition-all transform group-hover:-translate-y-4 relative overflow-hidden">
|
||||
<Image src="/assets/images/tokens/deny_leader.png" alt="Deny Leader" fill className="object-contain" />
|
||||
</div>
|
||||
<span className="block text-center text-white mt-2 font-bold bg-red-600 px-2 rounded uppercase tracking-widest">RECHAZAR</span>
|
||||
@@ -612,26 +599,19 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
{gameState.phase === GamePhase.MISSION && (
|
||||
<motion.div
|
||||
key="mission"
|
||||
className="fixed inset-0 flex items-center justify-center bg-black/90 z-50"
|
||||
className="fixed inset-0 flex items-start justify-center bg-black/90 z-50 pt-20"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
{gameState.proposedTeam.includes(currentPlayerId) ? (
|
||||
<div className="flex flex-col items-center gap-8 w-full max-w-6xl px-4">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4 drop-shadow-2xl text-center uppercase tracking-wider animate-pulse">
|
||||
🎯 ¡ESTÁS EN LA MISIÓN!
|
||||
<div className="flex flex-col items-center gap-4 w-full max-w-6xl px-4">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-2 drop-shadow-2xl text-center uppercase tracking-wider animate-pulse">
|
||||
🎯 REALIZA LA MISIÓN
|
||||
</h2>
|
||||
<p className="text-white text-xl mb-4 text-center">
|
||||
Elige el resultado de tu participación
|
||||
<p className="text-white text-xl mb-2 text-center">
|
||||
Elige si quieres un éxito o un fracaso
|
||||
</p>
|
||||
|
||||
{/* DEBUG INFO - TEMPORAL */}
|
||||
<div className="text-xs text-gray-400 bg-black/50 p-2 rounded mb-4">
|
||||
Debug: Tu ID: {currentPlayerId} | Equipo: [{gameState.proposedTeam.join(', ')}]
|
||||
<br />
|
||||
Tu facción: {currentPlayer?.faction || 'UNDEFINED'} | Rol: {currentPlayer?.role || 'UNDEFINED'}
|
||||
</div>
|
||||
|
||||
{/* Cartas en orden aleatorio */}
|
||||
<div className="flex gap-12 flex-wrap justify-center">
|
||||
{cardOrder ? (
|
||||
@@ -643,11 +623,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
disabled={missionVote !== null}
|
||||
>
|
||||
<motion.div
|
||||
className="w-64 h-96 bg-gradient-to-br from-blue-600 to-blue-900 rounded-2xl shadow-2xl border-4 border-blue-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:rotate-3 hover:shadow-blue-500/50"
|
||||
className="w-48 h-72 bg-gradient-to-br from-blue-600 to-blue-900 rounded-2xl shadow-2xl border-4 border-blue-400 flex flex-col items-center justify-center p-4 transform transition-all hover:scale-110 hover:rotate-3 hover:shadow-blue-500/50"
|
||||
whileHover={{ scale: 1.1, rotate: 3 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Image src="/assets/images/tokens/vote_approve.png" alt="Success" width={180} height={180} className="drop-shadow-2xl" />
|
||||
<Image src="/assets/images/tokens/vote_approve.png" alt="Success" width={120} height={120} className="drop-shadow-2xl" />
|
||||
<span className="mt-6 text-white font-bold text-2xl tracking-widest uppercase">ÉXITO</span>
|
||||
</motion.div>
|
||||
</button>
|
||||
@@ -660,11 +640,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
disabled={missionVote !== null}
|
||||
>
|
||||
<motion.div
|
||||
className="w-64 h-96 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
||||
className="w-48 h-72 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-4 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
||||
whileHover={{ scale: 1.1, rotate: -3 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Image src="/assets/images/tokens/vote_reject.png" alt="Fail" width={180} height={180} className="drop-shadow-2xl" />
|
||||
<Image src="/assets/images/tokens/vote_reject.png" alt="Fail" width={120} height={120} className="drop-shadow-2xl" />
|
||||
<span className="mt-6 text-white font-bold text-2xl tracking-widest uppercase">SABOTAJE</span>
|
||||
</motion.div>
|
||||
</button>
|
||||
@@ -680,11 +660,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
disabled={missionVote !== null}
|
||||
>
|
||||
<motion.div
|
||||
className="w-64 h-96 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
||||
className="w-48 h-72 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-4 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
||||
whileHover={{ scale: 1.1, rotate: -3 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Image src="/assets/images/tokens/vote_reject.png" alt="Fail" width={180} height={180} className="drop-shadow-2xl" />
|
||||
<Image src="/assets/images/tokens/vote_reject.png" alt="Fail" width={120} height={120} className="drop-shadow-2xl" />
|
||||
<span className="mt-6 text-white font-bold text-2xl tracking-widest uppercase">SABOTAJE</span>
|
||||
</motion.div>
|
||||
</button>
|
||||
@@ -697,11 +677,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
disabled={missionVote !== null}
|
||||
>
|
||||
<motion.div
|
||||
className="w-64 h-96 bg-gradient-to-br from-blue-600 to-blue-900 rounded-2xl shadow-2xl border-4 border-blue-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:rotate-3 hover:shadow-blue-500/50"
|
||||
className="w-48 h-72 bg-gradient-to-br from-blue-600 to-blue-900 rounded-2xl shadow-2xl border-4 border-blue-400 flex flex-col items-center justify-center p-4 transform transition-all hover:scale-110 hover:rotate-3 hover:shadow-blue-500/50"
|
||||
whileHover={{ scale: 1.1, rotate: 3 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Image src="/assets/images/tokens/vote_approve.png" alt="Success" width={180} height={180} className="drop-shadow-2xl" />
|
||||
<Image src="/assets/images/tokens/vote_approve.png" alt="Success" width={120} height={120} className="drop-shadow-2xl" />
|
||||
<span className="mt-6 text-white font-bold text-2xl tracking-widest uppercase">ÉXITO</span>
|
||||
</motion.div>
|
||||
</button>
|
||||
@@ -800,9 +780,9 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* JUGADORES (TIENDA DE CAMPAÑA - Adaptable) */}
|
||||
<div className="z-10 w-full py-2 bg-black/40 backdrop-blur-sm border-t border-white/10">
|
||||
<div className="flex flex-wrap justify-center items-center gap-2 md:gap-6 px-2 w-full">
|
||||
{/* JUGADORES - POSICIONADOS ABSOLUTAMENTE EN EL FONDO */}
|
||||
<div className="fixed bottom-[10px] left-0 right-0 z-50 bg-black/80 border-t border-white/10 backdrop-blur-md py-2">
|
||||
<div className="w-full px-4 flex flex-wrap items-center justify-center gap-4">
|
||||
{gameState.players.map((player) => {
|
||||
const isSelected = selectedTeam.includes(player.id);
|
||||
const isMe = player.id === currentPlayerId;
|
||||
@@ -815,16 +795,15 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
key={player.id}
|
||||
onClick={() => isLeader && gameState.phase === GamePhase.TEAM_BUILDING && toggleTeamSelection(player.id)}
|
||||
className={`
|
||||
relative flex flex-col items-center cursor-pointer transition-all duration-300
|
||||
${isSelected ? 'scale-110' : 'scale-100 opacity-80 hover:opacity-100'}
|
||||
relative flex flex-col items-center cursor-pointer transition-all duration-300 group
|
||||
${isSelected ? 'scale-110 z-10' : 'scale-100 opacity-70 hover:opacity-100 hover:scale-105'}
|
||||
`}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className={`
|
||||
w-12 h-12 md:w-20 md:h-20 rounded-full border-2 overflow-hidden relative shadow-lg bg-black
|
||||
${isSelected ? 'border-yellow-400 ring-2 md:ring-4 ring-yellow-400/30' : 'border-gray-400'}
|
||||
<div className="relative rounded-full border-2 overflow-hidden shadow-lg bg-black transition-all w-16 h-16
|
||||
${isSelected ? 'border-yellow-400 ring-4 ring-yellow-400/30 shadow-yellow-400/20' : 'border-gray-500 group-hover:border-gray-300'}
|
||||
${gameState.currentLeaderId === player.id ? 'ring-2 ring-white' : ''}
|
||||
`}>
|
||||
">
|
||||
<Image
|
||||
src={avatarSrc}
|
||||
alt={player.name}
|
||||
@@ -834,7 +813,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
{/* Icono de Líder */}
|
||||
{gameState.currentLeaderId === player.id && (
|
||||
<div className="absolute bottom-0 right-0 bg-yellow-500 rounded-full p-1 w-6 h-6 flex items-center justify-center text-xs text-black font-bold border border-white z-10">
|
||||
<div className="absolute bottom-0 right-0 bg-yellow-500 rounded-full p-1 w-6 h-6 flex items-center justify-center text-[10px] text-black font-bold border border-white z-20 shadow-sm">
|
||||
L
|
||||
</div>
|
||||
)}
|
||||
@@ -846,14 +825,17 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
gameState.phase === 'mission_reveal' as any ||
|
||||
gameState.phase === 'mission_result' as any
|
||||
) && (
|
||||
<div className="absolute top-0 left-0 bg-green-500 rounded-full p-1 w-6 h-6 flex items-center justify-center text-xs text-white font-bold border border-white z-10">
|
||||
<div className="absolute top-0 left-0 bg-green-500 rounded-full p-1 w-6 h-6 flex items-center justify-center text-xs text-white font-bold border border-white z-20">
|
||||
⭐
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nombre */}
|
||||
<span className={`mt-2 text-xs font-mono px-2 py-0.5 rounded ${isMe ? 'bg-blue-600 text-white' : 'bg-black/50 text-gray-300'}`}>
|
||||
<span className={`
|
||||
mt-1 text-xs font-mono px-2 py-0.5 rounded shadow-sm whitespace-nowrap max-w-[100px] truncate
|
||||
${isMe ? 'bg-blue-600 text-white font-bold' : 'bg-black/60 text-gray-300 border border-white/10'}
|
||||
`}>
|
||||
{player.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -924,7 +906,7 @@ function VotingTimer() {
|
||||
}, [timeLeft]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 bg-red-600/80 text-white w-16 h-16 rounded-full flex items-center justify-center border-4 border-red-400 animate-pulse text-2xl font-bold font-mono">
|
||||
<div className="fixed top-5 left-5 bg-red-600/80 text-white w-16 h-16 rounded-full flex items-center justify-center border-4 border-red-400 animate-pulse text-2xl font-bold font-mono">
|
||||
{timeLeft}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -236,13 +236,15 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-screen flex flex-col items-center overflow-hidden">
|
||||
<div className="relative w-full h-screen flex flex-col overflow-hidden">
|
||||
{/* Fondo */}
|
||||
<div className="absolute inset-0 z-0 opacity-40">
|
||||
<Image src="/assets/images/ui/bg_game.png" alt="Game Background" fill className="object-cover" />
|
||||
<div className="absolute inset-0 bg-black/60" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full flex flex-col items-center">
|
||||
{/* Contenedor principal con altura calculada */}
|
||||
<div className={`relative z-10 w-full flex flex-col items-center ${gameState.players.length > 6 ? 'h-[70%]' : 'h-[85%]'}`}>
|
||||
|
||||
{/* --- MAPA TÁCTICO (TABLERO) --- */}
|
||||
<div className="relative w-full max-w-5xl aspect-video mt-4 shadow-2xl border-4 border-gray-800 rounded-lg overflow-hidden bg-[#2a2a2a]">
|
||||
@@ -315,7 +317,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
</div>
|
||||
|
||||
{/* --- ÁREA DE JUEGO (CARTAS Y ACCIONES) --- */}
|
||||
<div className="flex-1 w-full max-w-6xl relative mt-4 px-4">
|
||||
<div className="flex-1 w-full max-w-6xl relative mt-4 px-4 overflow-y-auto">
|
||||
<AnimatePresence mode="wait">
|
||||
|
||||
{/* FASE: VOTACIÓN DE LÍDER */}
|
||||
@@ -488,10 +490,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JUGADORES (TIENDA DE CAMPAÑA) */}
|
||||
<div className="z-10 w-full overflow-x-auto pb-4">
|
||||
<div className="flex justify-center gap-4 min-w-max px-4">
|
||||
{/* Área de jugadores fija en el pie - Fuera del contenedor principal */}
|
||||
<div className={`z-20 w-full bg-black/80 border-t border-white/10 backdrop-blur-md ${gameState.players.length > 6 ? 'h-[30vh]' : 'h-[15vh]'}`}>
|
||||
<div className={`w-full h-full p-2 ${gameState.players.length > 6 ? 'grid grid-cols-5 auto-rows-fr gap-y-2 gap-x-4 content-center justify-items-center' : 'flex items-center justify-center gap-6'}`}>
|
||||
{gameState.players.map((player) => {
|
||||
const isSelected = selectedTeam.includes(player.id);
|
||||
const isMe = player.id === currentPlayerId;
|
||||
@@ -504,14 +507,15 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
key={player.id}
|
||||
onClick={() => isLeader && gameState.phase === GamePhase.TEAM_BUILDING && toggleTeamSelection(player.id)}
|
||||
className={`
|
||||
relative flex flex-col items-center cursor-pointer transition-all duration-300
|
||||
${isSelected ? 'scale-110' : 'scale-100 opacity-80 hover:opacity-100'}
|
||||
relative flex flex-col items-center cursor-pointer transition-all duration-300 group
|
||||
${isSelected ? 'scale-110 z-10' : 'scale-100 opacity-70 hover:opacity-100 hover:scale-105'}
|
||||
`}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className={`
|
||||
w-16 h-16 rounded-full border-2 overflow-hidden relative shadow-lg bg-black
|
||||
${isSelected ? 'border-yellow-400 ring-4 ring-yellow-400/30' : 'border-gray-400'}
|
||||
relative rounded-full border-2 overflow-hidden shadow-lg bg-black transition-all
|
||||
${gameState.players.length > 6 ? 'w-12 h-12' : 'w-20 h-20'}
|
||||
${isSelected ? 'border-yellow-400 ring-4 ring-yellow-400/30 shadow-yellow-400/20' : 'border-gray-500 group-hover:border-gray-300'}
|
||||
${player.isLeader ? 'ring-2 ring-white' : ''}
|
||||
`}>
|
||||
<Image
|
||||
@@ -523,16 +527,20 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
{/* Icono de Líder */}
|
||||
{player.isLeader && (
|
||||
<div className="absolute bottom-0 right-0 bg-yellow-500 rounded-full p-1 w-6 h-6 flex items-center justify-center text-xs text-black font-bold border border-white z-10">
|
||||
<div className={`absolute bottom-0 right-0 bg-yellow-500 rounded-full p-1 flex items-center justify-center text-[10px] text-black font-bold border border-white z-20 shadow-sm ${gameState.players.length > 6 ? 'w-5 h-5' : 'w-6 h-6'}`}>
|
||||
L
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nombre */}
|
||||
<span className={`mt-2 text-xs font-mono px-2 py-0.5 rounded ${isMe ? 'bg-blue-600 text-white' : 'bg-black/50 text-gray-300'}`}>
|
||||
<span className={`
|
||||
mt-1 text-xs font-mono px-2 py-0.5 rounded shadow-sm whitespace-nowrap max-w-[100px] truncate
|
||||
${isMe ? 'bg-blue-600 text-white font-bold' : 'bg-black/60 text-gray-300 border border-white/10'}
|
||||
`}>
|
||||
{player.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -540,5 +548,126 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
{/* HISTÓRICO DE MISIONES (Esquina superior derecha) */}
|
||||
{gameState.missionHistory.length > 0 && (
|
||||
<div className="absolute top-4 right-4 bg-black/80 p-3 rounded-lg border border-white/20 backdrop-blur-sm">
|
||||
<div className="absolute top-4 right-4 bg-black/80 p-3 rounded-lg border border-white/20 backdrop-blur-sm z-50">
|
||||
<div className="text-[10px] text-gray-400 uppercase mb-2 text-center font-bold tracking-wider">Historial</div>
|
||||
<div className="flex gap-2">
|
||||
{gameState.missionHistory.map((mission, idx) => {
|
||||
return (
|
||||
<div key={idx} className="relative">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold border-2 ${mission.isSuccess
|
||||
? 'bg-blue-600 border-blue-400 text-white'
|
||||
: 'bg-red-600 border-red-400 text-white'
|
||||
}`}
|
||||
title={`Misión ${mission.round}: ${mission.isSuccess ? 'Éxito' : 'Fracaso'} (${mission.successes}✓ ${mission.fails}✗)`}
|
||||
>
|
||||
{mission.round}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Subcomponente para el Timer de Votación
|
||||
function VotingTimer({ onTimeout }: { onTimeout: () => void }) {
|
||||
const [timeLeft, setTimeLeft] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
if (timeLeft <= 0) {
|
||||
onTimeout();
|
||||
return;
|
||||
}
|
||||
const interval = setInterval(() => setTimeLeft(t => t - 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [timeLeft, onTimeout]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 bg-red-600/80 text-white w-16 h-16 rounded-full flex items-center justify-center border-4 border-red-400 animate-pulse text-2xl font-bold font-mono">
|
||||
{timeLeft}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Subcomponente para la revelación de cartas de misión
|
||||
function MissionReveal({ votes, onComplete }: { votes: boolean[], onComplete: () => void }) {
|
||||
const [revealedCount, setRevealedCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (revealedCount < votes.length) {
|
||||
const timer = setTimeout(() => {
|
||||
setRevealedCount(prev => prev + 1);
|
||||
}, 1500);
|
||||
return () => clearTimeout(timer);
|
||||
} else if (revealedCount === votes.length) {
|
||||
const timer = setTimeout(() => {
|
||||
onComplete();
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [revealedCount, votes.length, onComplete]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex flex-col items-center gap-6"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold text-white mb-4 drop-shadow-lg">Revelando Votos...</h2>
|
||||
<div className="flex gap-4 flex-wrap justify-center">
|
||||
{votes.map((vote, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ rotateY: 180, opacity: 0 }}
|
||||
animate={idx < revealedCount ? { rotateY: 0, opacity: 1 } : { rotateY: 180, opacity: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0 }}
|
||||
className="w-32 h-48 relative"
|
||||
>
|
||||
<Image
|
||||
src={vote ? '/assets/images/tokens/mission_success.png' : '/assets/images/tokens/mission_fail.png'}
|
||||
alt={vote ? 'Success' : 'Fail'}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// Subcomponente para el resultado de la misión
|
||||
function MissionResult({ gameState, onContinue }: { gameState: GameState, onContinue: () => void }) {
|
||||
const lastResult = gameState.questResults[gameState.currentRound - 1];
|
||||
const lastMission = gameState.missionHistory[gameState.missionHistory.length - 1];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex flex-col items-center gap-6"
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
>
|
||||
<h2 className={`text-5xl font-bold mb-4 drop-shadow-lg ${lastResult ? 'text-blue-400' : 'text-red-400'}`}>
|
||||
{lastResult ? '¡MISIÓN EXITOSA!' : '¡MISIÓN FALLIDA!'}
|
||||
</h2>
|
||||
<div className="bg-black/80 p-6 rounded-lg border border-white/20">
|
||||
<div className="text-white text-xl mb-4">
|
||||
Votos de Éxito: <span className="text-blue-400 font-bold">{lastMission?.successes || 0}</span>
|
||||
</div>
|
||||
<div className="text-white text-xl">
|
||||
Votos de Sabotaje: <span className="text-red-400 font-bold">{lastMission?.fails || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onContinue}
|
||||
className="bg-white/20 hover:bg-white/40 border border-white px-8 py-3 rounded text-white font-bold uppercase tracking-widest backdrop-blur-sm transition-all"
|
||||
>
|
||||
Continuar
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ export default function MissionResult({ gameState, onContinue, isHost }: Mission
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="fixed inset-0 flex flex-col items-center justify-center bg-transparent z-50"
|
||||
className="fixed inset-0 flex flex-col items-center justify-center bg-transparent z-50 pt-8"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<motion.h2
|
||||
className={`text-6xl md:text-7xl font-bold mb-8 ${isSuccess ? 'text-blue-500' : 'text-red-500'}`}
|
||||
className={`text-5xl font-bold mb-8 ${isSuccess ? 'text-blue-500' : 'text-red-500'}`}
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
|
||||
|
||||
@@ -23,15 +23,15 @@ export default function MissionReveal({ votes, onFinished }: MissionRevealProps)
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<h2 className="text-5xl font-bold text-white mb-12 uppercase tracking-widest drop-shadow-lg">
|
||||
Resultado de Misión
|
||||
<h2 className="text-3xl font-bold text-white mb-12 uppercase tracking-widest drop-shadow-lg">
|
||||
Resultado de la misión
|
||||
</h2>
|
||||
|
||||
<div className="flex gap-8 justify-center mb-12 flex-wrap max-w-[90vw]">
|
||||
<div className="flex gap-4 justify-center mb-12 flex-wrap max-w-[90vw]">
|
||||
{votes.map((vote, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
className="w-48 h-72 rounded-xl flex items-center justify-center shadow-2xl relative overflow-hidden"
|
||||
className="w-32 h-48 rounded-xl flex items-center justify-center shadow-2xl relative overflow-hidden"
|
||||
initial={{ scale: 0, rotateY: 180 }}
|
||||
animate={{ scale: 1, rotateY: 0 }}
|
||||
transition={{
|
||||
|
||||
@@ -29,6 +29,7 @@ export class Game {
|
||||
|
||||
this.state = {
|
||||
roomId,
|
||||
roomName,
|
||||
phase: GamePhase.LOBBY,
|
||||
players: [],
|
||||
currentRound: 1,
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface MissionRecord {
|
||||
|
||||
export interface GameState {
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
phase: GamePhase;
|
||||
players: Player[];
|
||||
hostId: string;
|
||||
|
||||