feat: Sistema completo de fin de juego y pantallas de victoria

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
This commit is contained in:
Resistencia Dev
2025-12-08 13:41:44 +01:00
parent 774e1b982d
commit b836c53002
7 changed files with 234 additions and 11 deletions

View File

@@ -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 && (
<VictoryScreen
gameState={gameState}
isHost={isHost}
onRestart={() => actions.restartGame()}
onFinalize={() => actions.finalizeGame()}
/>
)}
{/* FASE: VICTORIA ALIADOS */}
{gameState.phase === GamePhase.ALLIED_WIN && (
<VictoryScreen
gameState={gameState}
isHost={isHost}
onRestart={() => actions.restartGame()}
onFinalize={() => actions.finalizeGame()}
/>
)}
</AnimatePresence>
</div>

View File

@@ -23,7 +23,7 @@ export default function MissionResult({ gameState, onContinue, isHost }: Mission
return (
<motion.div
className="fixed inset-0 flex flex-col items-center justify-center bg-black/90 z-50"
className="fixed inset-0 flex flex-col items-center justify-center bg-transparent z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>

View File

@@ -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 (
<motion.div
className="fixed inset-0 flex flex-col items-center justify-center bg-gradient-to-b from-black via-zinc-900 to-black z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{/* Título de victoria */}
<motion.div
className="text-center mb-12"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
>
<h1 className={`text-7xl md:text-8xl font-bold mb-4 ${isNazisWin ? 'text-red-600' : 'text-blue-500'}`}>
{isNazisWin ? '¡VICTORIA NAZI!' : '¡VICTORIA ALIADA!'}
</h1>
<p className="text-3xl text-gray-300">
{isNazisWin ? 'Los Nazis han conquistado Francia' : 'La Resistencia ha triunfado'}
</p>
</motion.div>
{/* Información del juego */}
<motion.div
className="bg-black/50 p-8 rounded-xl border border-white/20 mb-8 max-w-2xl"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.5 }}
>
<div className="grid grid-cols-2 gap-6 text-center">
<div>
<p className="text-gray-400 text-sm uppercase mb-2">Misiones Exitosas</p>
<p className="text-4xl font-bold text-blue-400">
{gameState.questResults.filter(r => r === true).length}
</p>
</div>
<div>
<p className="text-gray-400 text-sm uppercase mb-2">Misiones Fracasadas</p>
<p className="text-4xl font-bold text-red-400">
{gameState.questResults.filter(r => r === false).length}
</p>
</div>
</div>
</motion.div>
{/* Botones para el host */}
{isHost ? (
<motion.div
className="flex flex-col items-center gap-4"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 1 }}
>
<p className="text-yellow-500 font-bold text-xl mb-2">
Tiempo restante: {timeLeft}s
</p>
<div className="flex gap-4">
<motion.button
onClick={onRestart}
className="bg-green-600 hover:bg-green-500 text-white font-bold py-4 px-8 rounded-lg text-xl shadow-lg"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
🔄 NUEVA PARTIDA
</motion.button>
<motion.button
onClick={onFinalize}
className="bg-red-600 hover:bg-red-500 text-white font-bold py-4 px-8 rounded-lg text-xl shadow-lg"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
TERMINAR
</motion.button>
</div>
<p className="text-gray-500 text-sm mt-2">
Si no decides, la partida terminará automáticamente
</p>
</motion.div>
) : (
<motion.div
className="text-center"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 1 }}
>
<p className="text-white text-2xl font-mono bg-black/50 px-8 py-4 rounded-full border border-white/20 animate-pulse">
Esperando decisión del comandante...
</p>
<p className="text-gray-500 text-sm mt-4">
Tiempo restante: {timeLeft}s
</p>
</motion.div>
)}
</motion.div>
);
}