fix: Sistema de votación de líder completamente refactorizado

- Timer de 10 segundos que se reinicia correctamente al cambiar de líder
- Votación por mayoría calculada sobre votos emitidos, no total de jugadores
- Si nadie vota, líder rechazado automáticamente
- Alemanes pueden ver carta de sabotaje en misiones
- Reset correcto de selectedTeam al cambiar de fase
- Contador de votos fallidos incrementa correctamente
- Logs mejorados para debugging

Fixes:
- Timer visual se reinicia con key basada en currentLeaderId
- Facción verificada correctamente (Faction.ALEMANES vs 'spies')
- forceResolveLeaderVote llama a resolución con votos actuales
- selectedTeam se limpia al salir de TEAM_BUILDING
This commit is contained in:
Resistencia Dev
2025-12-08 00:05:08 +01:00
parent 9e0e343868
commit 06d2171871
21 changed files with 67 additions and 35 deletions

Binary file not shown.

Binary file not shown.

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: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
import { GameState, GamePhase, Player, GAME_CONFIG } from '../../../shared/types';
import { GameState, GamePhase, Player, GAME_CONFIG, Faction } from '../../../shared/types';
import MissionReveal from './MissionReveal';
import MissionResult from './MissionResult';
@@ -41,6 +41,13 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
}
}, [gameState.phase]);
// Reset selectedTeam cuando no estamos en TEAM_BUILDING o cambia el líder
useEffect(() => {
if (gameState.phase !== GamePhase.TEAM_BUILDING) {
setSelectedTeam([]);
}
}, [gameState.phase, gameState.currentLeaderId]);
const currentPlayer = gameState.players.find(p => p.id === currentPlayerId);
const isLeader = gameState.currentLeaderId === currentPlayerId; // FIX: Usar currentLeaderId del estado
@@ -368,9 +375,9 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
¿Aceptas a <span className="text-yellow-400">{gameState.players.find(p => p.id === gameState.currentLeaderId)?.name}</span> como Líder?
</div>
{/* Timer */}
{/* Timer visual (solo muestra el tiempo, el servidor controla el timeout) */}
{!gameState.leaderVotes?.[currentPlayerId] && (
<VotingTimer onTimeout={() => actions.voteLeader(null)} />
<VotingTimer key={gameState.currentLeaderId} />
)}
</div>
@@ -547,8 +554,8 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
</motion.div>
</button>
{/* Carta de Sabotaje segundo (solo para espías) */}
{currentPlayer?.faction === 'spies' && (
{/* Carta de Sabotaje segundo (solo para alemanes) */}
{currentPlayer?.faction === Faction.ALEMANES && (
<button onClick={() => handleMissionVote(false)} className="group transition-opacity" style={{ opacity: missionVote !== null && missionVote !== false ? 0.5 : 1 }} 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"
@@ -563,8 +570,8 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
</>
) : (
<>
{/* Carta de Sabotaje primero (solo para espías) */}
{currentPlayer?.faction === 'spies' && (
{/* Carta de Sabotaje primero (solo para alemanes) */}
{currentPlayer?.faction === Faction.ALEMANES && (
<button onClick={() => handleMissionVote(false)} className="group transition-opacity" style={{ opacity: missionVote !== null && missionVote !== false ? 0.5 : 1 }} 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"
@@ -713,18 +720,17 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
);
}
// Subcomponente para el Timer de Votación
function VotingTimer({ onTimeout }: { onTimeout: () => void }) {
// Subcomponente para el Timer de Votación (solo visual, el servidor controla el timeout real)
function VotingTimer() {
const [timeLeft, setTimeLeft] = useState(10);
useEffect(() => {
if (timeLeft <= 0) {
onTimeout();
return;
return; // El servidor se encargará de forzar la resolución
}
const interval = setInterval(() => setTimeLeft(t => t - 1), 1000);
return () => clearInterval(interval);
}, [timeLeft, onTimeout]);
}, [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">

View File

@@ -97,7 +97,7 @@ export const useSocket = () => {
proposeTeam,
voteTeam,
voteMission,
voteLeader: (approve: boolean | null) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
voteLeader: (approve: boolean) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
assassinKill,
finishIntro: () => socket?.emit('finish_intro', { roomId: gameState?.roomId }),
finishReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }),