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
This commit is contained in:
Resistencia Dev
2025-12-13 01:18:52 +01:00
parent c67f97845a
commit 13d56c2431
33 changed files with 229 additions and 115 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 764 KiB

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 720 KiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 716 KiB

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -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>

View File

@@ -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'}
${gameState.currentLeaderId === player.id ? 'ring-2 ring-white' : ''}
`}>
<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>
);

View File

@@ -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,57 +490,184 @@ 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">
{gameState.players.map((player) => {
const isSelected = selectedTeam.includes(player.id);
const isMe = player.id === currentPlayerId;
{/* Á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;
// Avatar logic
const avatarSrc = `/assets/images/characters/${player.avatar}`;
// Avatar logic
const avatarSrc = `/assets/images/characters/${player.avatar}`;
return (
<div
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 group
${isSelected ? 'scale-110 z-10' : 'scale-100 opacity-70 hover:opacity-100 hover:scale-105'}
`}
>
{/* Avatar */}
<div className={`
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
src={avatarSrc}
alt={player.name}
fill
className="object-cover"
/>
{/* Icono de Líder */}
{player.isLeader && (
<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-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>
</div>
{/* 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 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={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'}
`}
>
{/* 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'}
${player.isLeader ? 'ring-2 ring-white' : ''}
`}>
<Image
src={avatarSrc}
alt={player.name}
fill
className="object-cover"
/>
{/* 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">
L
</div>
)}
<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>
{/* 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'}`}>
{player.name}
</span>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
{/* 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="text-[10px] text-gray-400 uppercase mb-2 text-center font-bold tracking-wider">Historial</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>
);
}

View File

@@ -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 }}

View File

@@ -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={{

View File

@@ -29,6 +29,7 @@ export class Game {
this.state = {
roomId,
roomName,
phase: GamePhase.LOBBY,
players: [],
currentRound: 1,

View File

@@ -62,6 +62,7 @@ export interface MissionRecord {
export interface GameState {
roomId: string;
roomName: string;
phase: GamePhase;
players: Player[];
hostId: string;