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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user