diff --git a/client/public/assets/audio/Rondas.mp3 b/client/public/assets/audio/Rondas.mp3 index 27de5a1..d1fbc09 100644 Binary files a/client/public/assets/audio/Rondas.mp3 and b/client/public/assets/audio/Rondas.mp3 differ diff --git a/client/public/assets/audio/Rondas_corto.mp3 b/client/public/assets/audio/Rondas_corto.mp3 new file mode 100644 index 0000000..27de5a1 Binary files /dev/null and b/client/public/assets/audio/Rondas_corto.mp3 differ diff --git a/client/public/assets/audio/Rondas_original.mp3 b/client/public/assets/audio/Rondas_original.mp3 new file mode 100644 index 0000000..d1fbc09 Binary files /dev/null and b/client/public/assets/audio/Rondas_original.mp3 differ diff --git a/client/public/assets/images/tokens/accept_leader.jpg b/client/public/assets/images/tokens/accept_leader.jpg deleted file mode 100644 index 2c9f2d5..0000000 Binary files a/client/public/assets/images/tokens/accept_leader.jpg and /dev/null differ diff --git a/client/public/assets/images/tokens/deny_leader.jpg b/client/public/assets/images/tokens/deny_leader.jpg deleted file mode 100644 index 2e3eae8..0000000 Binary files a/client/public/assets/images/tokens/deny_leader.jpg and /dev/null differ diff --git a/client/public/assets/images/tokens/paret1.png b/client/public/assets/images/tokens/paret1.png new file mode 100644 index 0000000..bf4deff Binary files /dev/null and b/client/public/assets/images/tokens/paret1.png differ diff --git a/client/public/assets/images/tokens/paret2.jpg b/client/public/assets/images/tokens/paret2.jpg new file mode 100644 index 0000000..397fdb0 Binary files /dev/null and b/client/public/assets/images/tokens/paret2.jpg differ diff --git a/client/public/assets/images/tokens/vote_approve.png b/client/public/assets/images/tokens/vote_approve_original.png similarity index 100% rename from client/public/assets/images/tokens/vote_approve.png rename to client/public/assets/images/tokens/vote_approve_original.png diff --git a/client/public/assets/images/tokens/vote_aprove.png b/client/public/assets/images/tokens/vote_aprove.png new file mode 100644 index 0000000..812314d Binary files /dev/null and b/client/public/assets/images/tokens/vote_aprove.png differ diff --git a/client/public/assets/images/tokens/vote_reject.png b/client/public/assets/images/tokens/vote_reject.png index 08a2460..cb41e6e 100644 Binary files a/client/public/assets/images/tokens/vote_reject.png and b/client/public/assets/images/tokens/vote_reject.png differ diff --git a/client/public/assets/images/tokens/vote_reject_original.png b/client/public/assets/images/tokens/vote_reject_original.png new file mode 100644 index 0000000..08a2460 Binary files /dev/null and b/client/public/assets/images/tokens/vote_reject_original.png differ diff --git a/client/public/assets/images/ui/bg_game.png b/client/public/assets/images/ui/bg_game.png index f77fa05..0599102 100644 Binary files a/client/public/assets/images/ui/bg_game.png and b/client/public/assets/images/ui/bg_game.png differ diff --git a/client/public/assets/images/ui/bg_game_original.png b/client/public/assets/images/ui/bg_game_original.png new file mode 100644 index 0000000..f77fa05 Binary files /dev/null and b/client/public/assets/images/ui/bg_game_original.png differ diff --git a/client/src/components/GameBoard.tsx.backup b/client/src/components/GameBoard.tsx.backup deleted file mode 100644 index ad8c854..0000000 --- a/client/src/components/GameBoard.tsx.backup +++ /dev/null @@ -1,711 +0,0 @@ -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'; - -interface GameBoardProps { - gameState: GameState; - currentPlayerId: string; - actions: any; -} - -export default function GameBoard({ gameState, currentPlayerId, actions }: GameBoardProps) { - const [selectedTeam, setSelectedTeam] = useState([]); - - // Hooks para FASE REVEAL ROLE - const [revealCard, setRevealCard] = useState(false); - - // Timer para avanzar automáticamente en REVEAL_ROLE - useEffect(() => { - if (gameState.phase === 'reveal_role' as any) { - const timer = setTimeout(() => { - actions.finishReveal(); - }, 10000); - return () => clearTimeout(timer); - } - }, [gameState.phase, actions]); - - const currentPlayer = gameState.players.find(p => p.id === currentPlayerId); - const isLeader = currentPlayer?.isLeader; - const config = GAME_CONFIG[gameState.players.length as keyof typeof GAME_CONFIG]; - const currentQuestSize = config?.quests[gameState.currentRound - 1]; - - // Manejar selección de equipo - const toggleTeamSelection = (playerId: string) => { - if (selectedTeam.includes(playerId)) { - setSelectedTeam(selectedTeam.filter(id => id !== playerId)); - } else { - if (selectedTeam.length < currentQuestSize) { - setSelectedTeam([...selectedTeam, playerId]); - } - } - }; - - // 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 - ]; - - // --- UI/Efectos para FASES TEMPRANAS --- - const isHost = gameState.hostId === currentPlayerId; - - // FASE INTRO - if (gameState.phase === 'intro' as any) { - return ( -
-
- Battlefield -
-
- -

- Guerra Total -

- - {/* Audio Auto-Play */} -
- ); - } - - // FASE REVEAL ROLE NO HOOKS HERE - - if (gameState.phase === 'reveal_role' as any) { - // Determinar imagen basada en el rol - // Mapeo básico: - // Merlin -> good_merlin.png - // Percival -> good_percival.png - // Servant -> good_soldier_X.png (random) - // Assassin -> evil_assassin.png - // Morgana -> evil_morgana.png - // Mordred -> evil_mordred.png - // Oberon -> evil_oberon.png - // Minion -> evil_minion_X.png - - let roleImage = '/assets/images/characters/good_soldier_1.png'; // Default - - const role = currentPlayer?.role; - if (role === 'merlin') roleImage = '/assets/images/characters/good_merlin.png'; - else if (role === 'assassin') roleImage = '/assets/images/characters/evil_assassin.png'; - else if (role === 'percival') roleImage = '/assets/images/characters/good_percival.png'; - else if (role === 'morgana') roleImage = '/assets/images/characters/evil_morgana.png'; - else if (role === 'mordred') roleImage = '/assets/images/characters/evil_mordred.png'; - else if (role === 'oberon') roleImage = '/assets/images/characters/evil_oberon.png'; - else if (role === 'loyal_servant') { - // Random soldier 1-5 - const idx = (currentPlayerId.charCodeAt(0) % 5) + 1; - roleImage = `/assets/images/characters/good_soldier_${idx}.png`; - } - else if (role === 'minion') { - // Random minion 1-3 - const idx = (currentPlayerId.charCodeAt(0) % 3) + 1; - roleImage = `/assets/images/characters/evil_minion_${idx}.png`; - } - - return ( -
- {/* FONDO (Mismo que Roll Call) */} -
- Resistance HQ -
-
- -
-

- Tu Identidad Secreta -

- -

- Desliza hacia arriba para revelar -

- -
- {/* Carta Revelada (Fondo) */} -
- Role -
- {role?.replace('_', ' ')} -
-
- - {/* Reverso de Carta (Draggable) */} - { - // Reducir umbral a -50 para facilitar - if (info.offset.y < -50) { - setRevealCard(true); - } - }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98, cursor: 'grabbing' }} - animate={revealCard ? { y: -1000, opacity: 0 } : { y: 0, opacity: 1 }} - className="absolute inset-0 w-full h-full rounded-xl overflow-hidden shadow-2xl z-20 cursor-grab active:cursor-grabbing hover:ring-2 hover:ring-white/50 transition-all" - > - Card Back - -
- - -
-
- ); - } - - // FASE ROLL CALL - if (gameState.phase === 'roll_call' as any) { - return ( -
-
- Resistance HQ -
-
- -
-

- Pasando Lista... -

- - {isHost && ( -
-
- ); - } - - return ( -
-
- Game Background -
-
- -
- - {/* --- MAPA TÁCTICO (TABLERO) --- */} -
- Tactical Map - - {/* TOKENS SOBRE EL MAPA */} - {missionCoords.map((coord, idx) => { - const result = gameState.questResults[idx]; - const isCurrent = gameState.currentRound === idx + 1; - - return ( -
- {/* Marcador de Ronda Actual */} - {isCurrent && ( - - Current Round - - )} - - {/* Resultado de Misión (Éxito/Fracaso) */} - {result === true && ( - - Success - - )} - {result === false && ( - - Fail - - )} -
- ); - })} - - {/* TRACK DE VOTOS FALLIDOS (Pequeño indicador en la esquina inferior izquierda del mapa) */} -
-
Votos Rechazados
-
- {[...Array(5)].map((_, i) => ( -
- ))} -
-
-
- - {/* --- ÁREA DE JUEGO (CARTAS Y ACCIONES) --- */} -
- - - {/* FASE: VOTACIÓN DE LÍDER */} - {gameState.phase === 'vote_leader' as any && ( - -
-

- Confirmar Líder -

-
- ¿Aceptas a {gameState.players.find(p => p.id === gameState.currentLeaderId)?.name} como Líder? -
- - {/* Timer */} - {!gameState.leaderVotes?.[currentPlayerId] && ( - actions.voteLeader(null)} /> - )} -
- - {gameState.leaderVotes?.[currentPlayerId] === undefined ? ( -
- - -
- ) : ( -
- VOTO REGISTRADO. ESPERANDO AL RESTO... -
- )} -
- )} - - {/* FASE: CONSTRUCCIÓN DE EQUIPO */} - {gameState.phase === GamePhase.TEAM_BUILDING && ( - -

- {isLeader ? 'TU TURNO: ELIGE EQUIPO' : `ESPERANDO AL LÍDER...`} -

-

- Se necesitan {currentQuestSize} agentes para esta misión. -

- - {isLeader && ( - - )} -
- )} - - {/* FASE: VOTACIÓN DE EQUIPO */} - {gameState.phase === GamePhase.VOTING_TEAM && ( - -
-

PROPUESTA DE MISIÓN

-
- {gameState.proposedTeam.map(id => { - const p = gameState.players.find(pl => pl.id === id); - return ( -
- {p?.name} -
- ); - })} -
-
- - {!currentPlayer?.hasVoted ? ( -
- - -
- ) : ( -
- VOTO REGISTRADO. ESPERANDO AL RESTO... -
- )} -
- )} - - {/* FASE: MISIÓN */} - {gameState.phase === GamePhase.MISSION && ( - - {gameState.proposedTeam.includes(currentPlayerId) ? ( -
-

¡ESTÁS EN LA MISIÓN!

-
- - - {/* Solo los malos pueden sabotear */} - {currentPlayer?.faction === 'spies' && ( - - )} -
-
- ) : ( -
- La misión está en curso...
- Rezando por el éxito. -
- )} -
- )} - - {/* FASE: REVELACIÓN DE CARTAS */} - {gameState.phase === 'mission_reveal' as any && ( - isHost && actions.finishMissionReveal()} - /> - )} - - {/* FASE: RESULTADO DE MISIÓN */} - {gameState.phase === 'mission_result' as any && ( - isHost && actions.finishMissionResult()} - /> - )} - -
-
- - {/* JUGADORES (TIENDA DE CAMPAÑA) */} -
-
- {gameState.players.map((player) => { - const isSelected = selectedTeam.includes(player.id); - const isMe = player.id === currentPlayerId; - - // Avatar logic - const avatarSrc = `/assets/images/characters/${player.avatar}`; - - return ( -
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 */} -
- {player.name} - - {/* Icono de Líder */} - {player.isLeader && ( -
- L -
- )} -
- - {/* Nombre */} - - {player.name} - - ); - })} -
-
- - {/* HISTÓRICO DE MISIONES (Esquina superior derecha) */} - {gameState.missionHistory.length > 0 && ( -
-
Historial
-
- {gameState.missionHistory.map((mission, idx) => ( -
- {mission.round} -
- ))} -
-
- )} -
-
- ); -} - - -// 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 ( -
- {timeLeft} -
- ); -} - -// Componente para revelar cartas una a una -function MissionReveal({ votes, onComplete }: { votes: boolean[], onComplete: () => void }) { - const [revealedCount, setRevealedCount] = useState(0); - - useEffect(() => { - if (revealedCount < votes.length) { - const timer = setTimeout(() => { - setRevealedCount(c => c + 1); - }, 2000); // 2 segundos entre carta y carta - return () => clearTimeout(timer); - } else if (revealedCount === votes.length && votes.length > 0) { - // Todas reveladas, esperar 2s más y avanzar - const timer = setTimeout(() => { - onComplete(); - }, 2000); - return () => clearTimeout(timer); - } - }, [revealedCount, votes.length, onComplete]); - - return ( - -

- Revelando Votos... -

- -
- {votes.slice(0, revealedCount).map((vote, idx) => ( - - {vote - - ))} -
- -
- {revealedCount} / {votes.length} cartas reveladas -
-
- ); -} - -// Componente para mostrar el resultado de la misión -function MissionResult({ gameState, onContinue }: { gameState: any, onContinue: () => void }) { - const currentMission = gameState.missionHistory[gameState.missionHistory.length - 1]; - const isHost = gameState.hostId === gameState.players[0]?.id; // Simplificado - - useEffect(() => { - // Auto-avanzar después de 5 segundos - const timer = setTimeout(() => { - onContinue(); - }, 5000); - return () => clearTimeout(timer); - }, [onContinue]); - - if (!currentMission) return null; - - const { isSuccess, successes, fails, team, round } = currentMission; - - return ( - - {/* Título */} -
-

- {isSuccess ? '✓ MISIÓN EXITOSA' : '✗ MISIÓN FALLIDA'} -

-

Misión #{round}

-
- - {/* Estadísticas */} -
-
-
{successes}
-
Éxitos
-
-
-
{fails}
-
Sabotajes
-
-
- - {/* Equipo */} -
-

Equipo de Misión:

-
- {team.map((playerId: string) => { - const player = gameState.players.find((p: any) => p.id === playerId); - return ( -
- {player?.name || 'Desconocido'} -
- ); - })} -
-
- - {/* Mensaje */} -
- Continuando en breve... -
-
- ); -} diff --git a/client/src/components/GameBoard_temp.tsx b/client/src/components/GameBoard_temp.tsx deleted file mode 100644 index b4a74c0..0000000 --- a/client/src/components/GameBoard_temp.tsx +++ /dev/null @@ -1,673 +0,0 @@ -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'; - -interface GameBoardProps { - gameState: GameState; - currentPlayerId: string; - actions: any; -} - -export default function GameBoard({ gameState, currentPlayerId, actions }: GameBoardProps) { - const [selectedTeam, setSelectedTeam] = useState([]); - - // Hooks para FASE REVEAL ROLE - const [revealCard, setRevealCard] = useState(false); - - // Timer para avanzar automáticamente en REVEAL_ROLE - useEffect(() => { - if (gameState.phase === 'reveal_role' as any) { - const timer = setTimeout(() => { - actions.finishReveal(); - }, 10000); - return () => clearTimeout(timer); - } - }, [gameState.phase, actions]); - - const currentPlayer = gameState.players.find(p => p.id === currentPlayerId); - const isLeader = currentPlayer?.isLeader; - const config = GAME_CONFIG[gameState.players.length as keyof typeof GAME_CONFIG]; - const currentQuestSize = config?.quests[gameState.currentRound - 1]; - - // Manejar selección de equipo - const toggleTeamSelection = (playerId: string) => { - if (selectedTeam.includes(playerId)) { - setSelectedTeam(selectedTeam.filter(id => id !== playerId)); - } else { - if (selectedTeam.length < currentQuestSize) { - setSelectedTeam([...selectedTeam, playerId]); - } - } - }; - - // 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 - ]; - - // --- UI/Efectos para FASES TEMPRANAS --- - const isHost = gameState.hostId === currentPlayerId; - - // FASE INTRO - if (gameState.phase === 'intro' as any) { - return ( -
-
- Battlefield -
-
- -

- Guerra Total -

- - {/* Audio Auto-Play */} -
- ); - } - - // FASE REVEAL ROLE NO HOOKS HERE - - if (gameState.phase === 'reveal_role' as any) { - // Determinar imagen basada en el rol - // Mapeo básico: - // Merlin -> good_merlin.png - // Percival -> good_percival.png - // Servant -> good_soldier_X.png (random) - // Assassin -> evil_assassin.png - // Morgana -> evil_morgana.png - // Mordred -> evil_mordred.png - // Oberon -> evil_oberon.png - // Minion -> evil_minion_X.png - - let roleImage = '/assets/images/characters/good_soldier_1.png'; // Default - - const role = currentPlayer?.role; - if (role === 'merlin') roleImage = '/assets/images/characters/good_merlin.png'; - else if (role === 'assassin') roleImage = '/assets/images/characters/evil_assassin.png'; - else if (role === 'percival') roleImage = '/assets/images/characters/good_percival.png'; - else if (role === 'morgana') roleImage = '/assets/images/characters/evil_morgana.png'; - else if (role === 'mordred') roleImage = '/assets/images/characters/evil_mordred.png'; - else if (role === 'oberon') roleImage = '/assets/images/characters/evil_oberon.png'; - else if (role === 'loyal_servant') { - // Random soldier 1-5 - const idx = (currentPlayerId.charCodeAt(0) % 5) + 1; - roleImage = `/assets/images/characters/good_soldier_${idx}.png`; - } - else if (role === 'minion') { - // Random minion 1-3 - const idx = (currentPlayerId.charCodeAt(0) % 3) + 1; - roleImage = `/assets/images/characters/evil_minion_${idx}.png`; - } - - return ( -
- {/* FONDO (Mismo que Roll Call) */} -
- Resistance HQ -
-
- -
-

- Tu Identidad Secreta -

- -

- Desliza hacia arriba para revelar -

- -
- {/* Carta Revelada (Fondo) */} -
- Role -
- {role?.replace('_', ' ')} -
-
- - {/* Reverso de Carta (Draggable) */} - { - // Reducir umbral a -50 para facilitar - if (info.offset.y < -50) { - setRevealCard(true); - } - }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98, cursor: 'grabbing' }} - animate={revealCard ? { y: -1000, opacity: 0 } : { y: 0, opacity: 1 }} - className="absolute inset-0 w-full h-full rounded-xl overflow-hidden shadow-2xl z-20 cursor-grab active:cursor-grabbing hover:ring-2 hover:ring-white/50 transition-all" - > - Card Back - -
- - -
-
- ); - } - - // FASE ROLL CALL - if (gameState.phase === 'roll_call' as any) { - return ( -
-
- Resistance HQ -
-
- -
-

- Pasando Lista... -

- - {isHost && ( -
-
- ); - } - - return ( -
- {/* Fondo */} -
- Game Background -
-
- - {/* Contenedor principal con altura calculada */} -
6 ? 'h-[70%]' : 'h-[85%]'}`}> - - {/* --- MAPA TÁCTICO (TABLERO) --- */} -
- Tactical Map - - {/* TOKENS SOBRE EL MAPA */} - {missionCoords.map((coord, idx) => { - const result = gameState.questResults[idx]; - const isCurrent = gameState.currentRound === idx + 1; - - return ( -
- {/* Marcador de Ronda Actual */} - {isCurrent && ( - - Current Round - - )} - - {/* Resultado de Misión (Éxito/Fracaso) */} - {result === true && ( - - Success - - )} - {result === false && ( - - Fail - - )} -
- ); - })} - - {/* TRACK DE VOTOS FALLIDOS (Pequeño indicador en la esquina inferior izquierda del mapa) */} -
-
Votos Rechazados
-
- {[...Array(5)].map((_, i) => ( -
- ))} -
-
-
- - {/* --- ÁREA DE JUEGO (CARTAS Y ACCIONES) --- */} -
- - - {/* FASE: VOTACIÓN DE LÍDER */} - {gameState.phase === 'vote_leader' as any && ( - -
-

- Confirmar Líder -

-
- ¿Aceptas a {gameState.players.find(p => p.id === gameState.currentLeaderId)?.name} como Líder? -
- - {/* Timer */} - {!gameState.leaderVotes?.[currentPlayerId] && ( - actions.voteLeader(null)} /> - )} -
- - {gameState.leaderVotes?.[currentPlayerId] === undefined ? ( -
- - -
- ) : ( -
- VOTO REGISTRADO. ESPERANDO AL RESTO... -
- )} -
- )} - - {/* FASE: CONSTRUCCIÓN DE EQUIPO */} - {gameState.phase === GamePhase.TEAM_BUILDING && ( - -

- {isLeader ? 'TU TURNO: ELIGE EQUIPO' : `ESPERANDO AL LÍDER...`} -

-

- Se necesitan {currentQuestSize} agentes para esta misión. -

- - {isLeader && ( - - )} -
- )} - - {/* FASE: VOTACIÓN DE EQUIPO */} - {gameState.phase === GamePhase.VOTING_TEAM && ( - -
-

PROPUESTA DE MISIÓN

-
- {gameState.proposedTeam.map(id => { - const p = gameState.players.find(pl => pl.id === id); - return ( -
- {p?.name} -
- ); - })} -
-
- - {!currentPlayer?.hasVoted ? ( -
- - -
- ) : ( -
- VOTO REGISTRADO. ESPERANDO AL RESTO... -
- )} -
- )} - - {/* FASE: MISIÓN */} - {gameState.phase === GamePhase.MISSION && ( - - {gameState.proposedTeam.includes(currentPlayerId) ? ( -
-

¡ESTÁS EN LA MISIÓN!

-
- - - {/* Solo los malos pueden sabotear */} - {currentPlayer?.faction === 'spies' && ( - - )} -
-
- ) : ( -
- La misión está en curso...
- Rezando por el éxito. -
- )} -
- )} - - {/* FASE: REVELACIÓN DE CARTAS */} - {gameState.phase === 'mission_reveal' as any && ( - isHost && actions.finishMissionReveal()} - /> - )} - - {/* FASE: RESULTADO DE MISIÓN */} - {gameState.phase === 'mission_result' as any && ( - isHost && actions.finishMissionResult()} - /> - )} - -
-
-
- - {/* Área de jugadores fija en el pie - Fuera del contenedor principal */} -
6 ? 'h-[30vh]' : 'h-[15vh]'}`}> -
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}`; - - return ( -
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 */} -
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' : ''} - `}> - {player.name} - - {/* Icono de Líder */} - {player.isLeader && ( -
6 ? 'w-5 h-5' : 'w-6 h-6'}`}> - L -
- )} -
- - {/* Nombre */} - - {player.name} - -
- ); - })} -
-
- - {/* HISTÓRICO DE MISIONES (Esquina superior derecha) */} - {gameState.missionHistory.length > 0 && ( -
-
Historial
-
- {gameState.missionHistory.map((mission, idx) => { - return ( -
-
- {mission.round} -
-
- ); - })} -
-
- )} -
- ); -} - -// 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 ( -
- {timeLeft} -
- ); -} - -// 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 ( - -

Revelando Votos...

-
- {votes.map((vote, idx) => ( - - {vote - - ))} -
-
- ); -} - -// 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 ( - -

- {lastResult ? '¡MISIÓN EXITOSA!' : '¡MISIÓN FALLIDA!'} -

-
-
- Votos de Éxito: {lastMission?.successes || 0} -
-
- Votos de Sabotaje: {lastMission?.fails || 0} -
-
- -
- ); -} diff --git a/deploy copy.sh b/deploy copy.sh new file mode 100755 index 0000000..149b09a --- /dev/null +++ b/deploy copy.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# Script de deployment para Francia Ocupada +# Este script se ejecuta en el HOST, no en el runner + +set -e # Salir si hay algún error + +echo "🚀 Iniciando deployment de Francia Ocupada..." +echo "================================================" + +# Directorio del proyecto +PROJECT_DIR="/home/marti/docker/FranciaOcupada" +cd "$PROJECT_DIR" + +echo "📂 Directorio de trabajo: $(pwd)" +echo "" + +# PASO 1: Actualizar código desde Git +echo "📥 PASO 1: Actualizando código desde Git..." +git fetch origin +git reset --hard origin/main +echo "✅ Código actualizado" +echo "" + +# PASO 2: Detener contenedores anteriores +echo "🛑 PASO 2: Deteniendo contenedores anteriores..." +docker compose -f docker-compose_prod.yml down || true +docker container prune -f || true +echo "✅ Contenedores anteriores detenidos" +echo "" + +# PASO 3: Limpiar imágenes antiguas +echo "🧹 PASO 3: Limpiando imágenes antiguas..." +docker image prune -f || true +echo "✅ Limpieza completada" +echo "" + +# PASO 4: Construir imágenes Docker +echo "🔨 PASO 4: Construyendo imágenes Docker..." +docker compose -f docker-compose_prod.yml build --no-cache + +# Etiquetar con timestamp para trazabilidad +TAG_VERSION=$(date +%Y%m%d_%H%M%S) +docker tag resistencia-client:latest resistencia-client:${TAG_VERSION} || true +docker tag resistencia-server:latest resistencia-server:${TAG_VERSION} || true + +echo "✅ Imágenes construidas:" +echo " - resistencia-client:latest (${TAG_VERSION})" +echo " - resistencia-server:latest (${TAG_VERSION})" +echo "" + +# PASO 5: Desplegar contenedores +echo "📦 PASO 5: Desplegando aplicación..." +docker compose -f docker-compose_prod.yml up -d + +echo "✅ Aplicación desplegada exitosamente" +echo "" + +# PASO 6: Verificar deployment +echo "✅ PASO 6: Verificando deployment..." +sleep 10 + +echo "📊 Estado de los contenedores:" +docker compose -f docker-compose_prod.yml ps +echo "" + +# Verificar que los contenedores están corriendo +RUNNING_CONTAINERS=$(docker compose -f docker-compose_prod.yml ps -q | wc -l) +if [ "$RUNNING_CONTAINERS" -eq 0 ]; then + echo "❌ ERROR: No hay contenedores corriendo" + docker compose -f docker-compose_prod.yml logs + exit 1 +fi + +echo "✅ Verificación completada - $RUNNING_CONTAINERS contenedores corriendo" +echo "" + +# PASO 7: Mostrar logs recientes +echo "📋 PASO 7: Logs recientes..." +echo "" +echo "--- Logs del Cliente (últimas 20 líneas) ---" +docker compose -f docker-compose_prod.yml logs --tail=20 client || true +echo "" +echo "--- Logs del Servidor (últimas 20 líneas) ---" +docker compose -f docker-compose_prod.yml logs --tail=20 server || true +echo "" + +echo "================================================" +echo "🎉 Deployment completado exitosamente!" +echo "" +echo "🌐 Aplicación disponible en:" +echo " - Frontend: https://franciaocupada.martivich.es" +echo " - API: https://api.franciaocupada.martivich.es" +echo "================================================"