From b836c53002f31a7f9ac07c9f7def830863eb0a06 Mon Sep 17 00:00:00 2001 From: Resistencia Dev Date: Mon, 8 Dec 2025 13:41:44 +0100 Subject: [PATCH] feat: Sistema completo de fin de juego y pantallas de victoria MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevas funcionalidades: - Pantallas de victoria diferenciadas (NAZIS_WIN / ALLIED_WIN) * Diseño visual diferenciado (rojo para Nazis, azul para Aliados) * Timer de 30 segundos con auto-finalización * Estadísticas de misiones (exitosas vs fracasadas) * Opciones para el host: NUEVA PARTIDA o TERMINAR * Mensaje de espera para jugadores no-host - Sistema de reinicio de partida * Método restartGame() que resetea todas las variables * Reasigna roles y líder aleatorios * Vuelve a fase REVEAL_ROLE manteniendo jugadores - Sistema de finalización y expulsión * Método finalizeGame() que expulsa a todos después de 5s * Auto-expulsión si el host no decide en 30s * Limpieza de partida del servidor Mejoras en MISSION_RESULT: - Eliminado oscurecimiento de fondo (bg-transparent) - Tiempo de visualización aumentado de 5 a 7 segundos - Ahora se puede ver claramente el tablero con las fichas Lógica de transiciones: - 3 misiones fracasadas → NAZIS_WIN - 3 misiones exitosas → ASSASSIN_PHASE * Asesino acierta (mata a Marlene) → NAZIS_WIN * Asesino falla → ALLIED_WIN Archivos modificados: - shared/types.ts: Nuevas fases NAZIS_WIN y ALLIED_WIN - server/src/models/Game.ts: Métodos restartGame() y finalizeGame() - server/src/index.ts: Eventos restart_game y finalize_game - client/src/hooks/useSocket.ts: Acciones restartGame() y finalizeGame() - client/src/components/GameBoard.tsx: Renderizado de VictoryScreen - client/src/components/MissionResult.tsx: Sin oscurecimiento, 7s - client/src/components/VictoryScreen.tsx: NUEVO componente --- client/src/components/GameBoard.tsx | 25 ++++- client/src/components/MissionResult.tsx | 2 +- client/src/components/VictoryScreen.tsx | 126 ++++++++++++++++++++++++ client/src/hooks/useSocket.ts | 4 +- server/src/index.ts | 27 ++++- server/src/models/Game.ts | 57 ++++++++++- shared/types.ts | 4 +- 7 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 client/src/components/VictoryScreen.tsx diff --git a/client/src/components/GameBoard.tsx b/client/src/components/GameBoard.tsx index 95263a9..6837b1b 100644 --- a/client/src/components/GameBoard.tsx +++ b/client/src/components/GameBoard.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import { GameState, GamePhase, Player, GAME_CONFIG, Faction } from '../../../shared/types'; import MissionReveal from './MissionReveal'; import MissionResult from './MissionResult'; +import VictoryScreen from './VictoryScreen'; interface GameBoardProps { gameState: GameState; @@ -51,13 +52,13 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB // Estado para controlar cuándo mostrar el tablero const [showBoard, setShowBoard] = useState(false); - // Mostrar tablero solo 5 segundos después de MISSION_RESULT + // Mostrar tablero 7 segundos después de MISSION_RESULT useEffect(() => { if (gameState.phase === GamePhase.MISSION_RESULT) { setShowBoard(true); const timer = setTimeout(() => { setShowBoard(false); - }, 5000); // 5 segundos + }, 7000); // 7 segundos return () => clearTimeout(timer); } else { setShowBoard(false); @@ -679,6 +680,26 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB /> )} + {/* FASE: VICTORIA NAZIS */} + {gameState.phase === GamePhase.NAZIS_WIN && ( + actions.restartGame()} + onFinalize={() => actions.finalizeGame()} + /> + )} + + {/* FASE: VICTORIA ALIADOS */} + {gameState.phase === GamePhase.ALLIED_WIN && ( + actions.restartGame()} + onFinalize={() => actions.finalizeGame()} + /> + )} + diff --git a/client/src/components/MissionResult.tsx b/client/src/components/MissionResult.tsx index 6495715..8f351a3 100644 --- a/client/src/components/MissionResult.tsx +++ b/client/src/components/MissionResult.tsx @@ -23,7 +23,7 @@ export default function MissionResult({ gameState, onContinue, isHost }: Mission return ( diff --git a/client/src/components/VictoryScreen.tsx b/client/src/components/VictoryScreen.tsx new file mode 100644 index 0000000..b5c0220 --- /dev/null +++ b/client/src/components/VictoryScreen.tsx @@ -0,0 +1,126 @@ +import { motion } from 'framer-motion'; +import { useState, useEffect } from 'react'; +import { GameState, Faction } from '../../../shared/types'; + +interface VictoryScreenProps { + gameState: GameState; + isHost: boolean; + onRestart: () => void; + onFinalize: () => void; +} + +export default function VictoryScreen({ gameState, isHost, onRestart, onFinalize }: VictoryScreenProps) { + const [timeLeft, setTimeLeft] = useState(30); + const isNazisWin = gameState.winner === Faction.ALEMANES; + + // Timer de 30 segundos + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(prev => { + if (prev <= 1) { + // Se acabó el tiempo, finalizar automáticamente + onFinalize(); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, [onFinalize]); + + return ( + + {/* Título de victoria */} + +

+ {isNazisWin ? '¡VICTORIA NAZI!' : '¡VICTORIA ALIADA!'} +

+

+ {isNazisWin ? 'Los Nazis han conquistado Francia' : 'La Resistencia ha triunfado'} +

+
+ + {/* Información del juego */} + +
+
+

Misiones Exitosas

+

+ {gameState.questResults.filter(r => r === true).length} +

+
+
+

Misiones Fracasadas

+

+ {gameState.questResults.filter(r => r === false).length} +

+
+
+
+ + {/* Botones para el host */} + {isHost ? ( + +

+ Tiempo restante: {timeLeft}s +

+
+ + 🔄 NUEVA PARTIDA + + + ❌ TERMINAR + +
+

+ Si no decides, la partida terminará automáticamente +

+
+ ) : ( + +

+ Esperando decisión del comandante... +

+

+ Tiempo restante: {timeLeft}s +

+
+ )} +
+ ); +} diff --git a/client/src/hooks/useSocket.ts b/client/src/hooks/useSocket.ts index b341751..c1292c5 100644 --- a/client/src/hooks/useSocket.ts +++ b/client/src/hooks/useSocket.ts @@ -103,7 +103,9 @@ export const useSocket = () => { finishReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }), finishRollCall: () => socket?.emit('finish_roll_call', { roomId: gameState?.roomId }), finishMissionReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }), - finishMissionResult: () => socket?.emit('finish_mission_result', { roomId: gameState?.roomId }) + finishMissionResult: () => socket?.emit('finish_mission_result', { roomId: gameState?.roomId }), + restartGame: () => socket?.emit('restart_game', { roomId: gameState?.roomId }), + finalizeGame: () => socket?.emit('finalize_game', { roomId: gameState?.roomId }) } }; }; diff --git a/server/src/index.ts b/server/src/index.ts index cc616bf..f77df69 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -274,7 +274,32 @@ io.on('connection', (socket) => { } }); - // 7. DESCONEXIÓN + // 7. REINICIAR PARTIDA + socket.on('restart_game', ({ roomId }) => { + const game = games[roomId]; + if (game && game.hostId === socket.id) { + game.restartGame(); + io.to(roomId).emit('game_state', game.state); + } + }); + + // 8. FINALIZAR Y EXPULSAR JUGADORES + socket.on('finalize_game', ({ roomId }) => { + const game = games[roomId]; + if (game && game.hostId === socket.id) { + game.finalizeGame(); + io.to(roomId).emit('game_state', game.state); + + // Esperar 5 segundos y luego eliminar la partida + setTimeout(() => { + delete games[roomId]; + // Desconectar a todos los jugadores de la sala + io.in(roomId).socketsLeave(roomId); + }, 5000); + } + }); + + // 9. DESCONEXIÓN socket.on('disconnect', () => { // Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos) console.log('Desconectado:', socket.id); diff --git a/server/src/models/Game.ts b/server/src/models/Game.ts index fc8f951..0130b68 100644 --- a/server/src/models/Game.ts +++ b/server/src/models/Game.ts @@ -312,10 +312,14 @@ export class Game { const failures = this.state.questResults.filter(r => r === false).length; if (failures >= 3) { - this.endGame(Faction.ALEMANES, 'Tres misiones han fracasado.'); // Updated Faction + // Los Nazis ganan directamente + this.state.winner = Faction.ALEMANES; + this.state.phase = GamePhase.NAZIS_WIN; + this.log('¡Los Nazis han ganado! Tres misiones han fracasado.'); } else if (successes >= 3) { + // Los Aliados han completado 3 misiones, pero el Asesino tiene una oportunidad this.state.phase = GamePhase.ASSASSIN_PHASE; - this.log('¡La Resistencia ha triunfado! Pero el Asesino tiene una última oportunidad...'); + this.log('¡La Resistencia ha triunfado! Pero el Francotirador tiene una última oportunidad...'); } else { // Siguiente ronda this.state.currentRound++; @@ -328,10 +332,16 @@ export class Game { assassinKill(targetId: string) { const target = this.state.players.find(p => p.id === targetId); - if (target && target.role === Role.MARLENE) { // Updated Role - this.endGame(Faction.ALEMANES, '¡El Asesino ha eliminado a Marlene!'); // Updated Faction and message + if (target && target.role === Role.MARLENE) { + // El Francotirador acierta: Nazis ganan + this.state.winner = Faction.ALEMANES; + this.state.phase = GamePhase.NAZIS_WIN; + this.log('¡El Francotirador ha eliminado a Marlene! Los Nazis ganan.'); } else { - this.endGame(Faction.ALIADOS, 'El Asesino ha fallado. ¡La Resistencia gana!'); // Updated Faction + // El Francotirador falla: Aliados ganan + this.state.winner = Faction.ALIADOS; + this.state.phase = GamePhase.ALLIED_WIN; + this.log('El Francotirador ha fallado. ¡La Resistencia gana!'); } } @@ -354,6 +364,43 @@ export class Game { this.log(`FIN DEL JUEGO. Victoria para ${winner}. Razón: ${reason}`); } + // Método para reiniciar la partida (volver a REVEAL_ROLE) + restartGame() { + this.log('=== REINICIANDO PARTIDA ==='); + + // Resetear variables de juego + this.state.currentRound = 1; + this.state.failedVotesCount = 0; + this.state.questResults = [null, null, null, null, null]; + this.state.leaderVotes = {}; + this.state.proposedTeam = []; + this.state.teamVotes = {}; + this.state.missionVotes = []; + this.state.revealedVotes = []; + this.state.missionHistory = []; + this.state.winner = undefined; + + // Reasignar roles + const count = this.state.players.length; + const config = GAME_CONFIG[count as keyof typeof GAME_CONFIG]; + this.assignRoles(config.good, config.evil); + + // Asignar nuevo líder aleatorio + const leaderIndex = Math.floor(Math.random() * count); + this.state.players.forEach((p, i) => p.isLeader = i === leaderIndex); + this.state.currentLeaderId = this.state.players[leaderIndex].id; + + // Volver a REVEAL_ROLE + this.state.phase = GamePhase.REVEAL_ROLE; + this.log('Nueva partida iniciada. Revelando roles...'); + } + + // Método para finalizar definitivamente y preparar expulsión + finalizeGame() { + this.state.phase = GamePhase.GAME_OVER; + this.log('Partida finalizada. Los jugadores serán expulsados.'); + } + private log(message: string) { this.state.history.push(message); // Mantener solo los últimos 50 mensajes diff --git a/shared/types.ts b/shared/types.ts index ed3a626..dcbc00e 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -28,7 +28,9 @@ export enum GamePhase { MISSION = 'mission', // Los elegidos votan éxito/fracaso MISSION_REVEAL = 'mission_reveal', // Mostrar cartas una a una MISSION_RESULT = 'mission_result', // Pantalla de resumen - ASSASSIN_PHASE = 'assassin_phase', // Si gana el bien, el asesino intenta matar a Merlín + ASSASSIN_PHASE = 'assassin_phase', // Si gana el bien, el asesino intenta matar a Marlene + NAZIS_WIN = 'nazis_win', // Pantalla de victoria de los Nazis + ALLIED_WIN = 'allied_win', // Pantalla de victoria de los Aliados GAME_OVER = 'game_over', }