import express from 'express'; import http from 'http'; import { Server } from 'socket.io'; import cors from 'cors'; import dotenv from 'dotenv'; import crypto from 'crypto'; import { Game } from './models/Game'; import { GamePhase } from '../../shared/types'; dotenv.config(); const app = express(); const port = process.env.PORT || 4000; app.use(cors({ origin: process.env.CORS_ORIGIN || "http://localhost:3000", methods: ["GET", "POST"] })); const server = http.createServer(app); const io = new Server(server, { cors: { origin: process.env.CORS_ORIGIN || "http://localhost:3000", methods: ["GET", "POST"] } }); // ALMACÉN DE PARTIDAS (En memoria por ahora) // En el futuro esto podría estar en Redis o Postgres const games: Record = {}; // Almacén de timers para auto-resolución de votaciones const voteTimers: Record = {}; // --- LOBBY MANAGEMENT --- const MISSION_NAMES = [ "Operación Overlord", "Operación Market Garden", "Operación Barbarroja", "Operación Valkiria", "Operación Torch", "Operación Husky", "Batalla de Stalingrado", "Dunkerque", "El Alamein", "Midway", "Operación Ciudadela", "Operación Dragoon", "Operación Fortaleza", "Operación Eiche", "Operación León Marino", "Operación Urano" ]; // Helper para iniciar timer de votación de líder (10 segundos) function startLeaderVoteTimer(roomId: string) { if (voteTimers[roomId]) clearTimeout(voteTimers[roomId]); voteTimers[roomId] = setTimeout(() => { const g = games[roomId]; if (g && g.state.phase === 'vote_leader') { // Forzar resolución de votos cuando se acaba el tiempo g.forceResolveLeaderVote(); io.to(roomId).emit('game_state', g.state); // Si sigue en vote_leader (líder rechazado, nuevo líder), reiniciar timer if (g.state.phase === 'vote_leader') { startLeaderVoteTimer(roomId); } } }, 10000); // 10 segundos } const generateRoomName = () => { const idx = Math.floor(Math.random() * MISSION_NAMES.length); const suffix = Math.floor(100 + Math.random() * 900); // 3 digit code return `${MISSION_NAMES[idx]} #${suffix}`; }; io.on('connection', (socket) => { console.log('Cliente conectado:', socket.id); // Enviar lista de salas al conectar socket.emit('rooms_list', getRoomsList()); // A. CREAR SALA socket.on('create_game', ({ hostName, maxPlayers, password }) => { console.log(`[CREATE_GAME] Host: ${hostName}, Players: ${maxPlayers}`); const roomId = crypto.randomUUID(); const roomName = generateRoomName(); const newGame = new Game(roomId, roomName, socket.id, maxPlayers, password); games[roomId] = newGame; // Unir al creador automáticamente newGame.addPlayer(socket.id, hostName); socket.join(roomId); // Notificar al creador socket.emit('game_joined', { roomId, state: newGame.state }); // Actualizar lista a todos io.emit('rooms_list', getRoomsList()); }); // B. UNIRSE A SALA socket.on('join_game', ({ roomId, playerName, password }) => { const game = games[roomId]; if (!game) { socket.emit('error', 'La partida no existe'); return; } if (game.password && game.password !== password) { socket.emit('error', 'Contraseña incorrecta'); return; } if (game.state.players.length >= game.maxPlayers) { socket.emit('error', 'La partida está llena'); return; } // Evitar duplicados const existingPlayer = game.state.players.find(p => p.id === socket.id); if (!existingPlayer) { game.addPlayer(socket.id, playerName); } socket.join(roomId); // Enviar estado actualizado a la sala io.to(roomId).emit('game_state', game.state); // Avisar al usuario que entró OK socket.emit('game_joined', { roomId, state: game.state }); // Actualizar lista de salas (cambió contador de jugadores) io.emit('rooms_list', getRoomsList()); }); // C. REFRESCAR LISTA socket.on('get_rooms', () => { socket.emit('rooms_list', getRoomsList()); }); // --- GAMEPLAY DE SIEMPRE (Adaptado a roomId dinámico) --- // 2. INICIAR PARTIDA - FASE INTRO socket.on('start_game', ({ roomId }) => { const game = games[roomId]; // Solo el host puede iniciar if (game && game.hostId === socket.id && game.state.phase === 'lobby') { game.state.phase = 'intro' as any; io.to(roomId).emit('game_state', game.state); io.emit('rooms_list', getRoomsList()); } }); // 2.1 PASAR INTRO -> REVEAL ROLE socket.on('finish_intro', ({ roomId }) => { const game = games[roomId]; if (game && game.hostId === socket.id && game.state.phase === 'intro') { // AQUI DISTRIBUIMOS ROLES if (game.startGame()) { // startGame pone phase en TEAM_BUILDING, pero nosotros queremos REVEAL_ROLE primero game.state.phase = 'reveal_role' as any; io.to(roomId).emit('game_state', game.state); } } }); // 2.2 REVEAL ROLE -> ROLL CALL socket.on('finish_reveal', ({ roomId }) => { const game = games[roomId]; // Cualquiera puede llamar a esto si es auto-timer, o solo host? // El usuario dijo "Habrá una cuenta atrás de 5 segundos". // Lo ideal es que el Host controle el tiempo para sincronizar. if (game && game.hostId === socket.id && game.state.phase === 'reveal_role') { game.state.phase = 'roll_call' as any; io.to(roomId).emit('game_state', game.state); } }); // 2.3 FINALIZAR ROLL CALL -> PRIMER TURNO DE JUEGO socket.on('finish_roll_call', ({ roomId }) => { const game = games[roomId]; if (game && game.hostId === socket.id && game.state.phase === 'roll_call') { // ERROR CORREGIDO: No llamar a startGame() de nuevo porque re-baraja los roles. // Simplemente avanzamos a la fase de votación de líder que ya estaba configurada. game.state.phase = 'vote_leader' as any; // Iniciar timer de 11 segundos para forzar cambio de líder startLeaderVoteTimer(roomId); io.to(roomId).emit('game_state', game.state); } }); // 2.4 VOTAR LÍDER socket.on('vote_leader', ({ roomId, approve }) => { const game = games[roomId]; if (game) { const previousPhase = game.state.phase; game.voteLeader(socket.id, approve); io.to(roomId).emit('game_state', game.state); // Si cambió de fase (líder aprobado o rechazado) if (game.state.phase !== previousPhase) { // Limpiar timer actual if (voteTimers[roomId]) { clearTimeout(voteTimers[roomId]); delete voteTimers[roomId]; } // Si pasó a vote_leader de nuevo (líder rechazado), iniciar nuevo timer if (game.state.phase === 'vote_leader') { startLeaderVoteTimer(roomId); } } } }); // 3. PROPONER EQUIPO socket.on('propose_team', ({ roomId, teamIds }) => { const game = games[roomId]; if (game && game.proposeTeam(teamIds)) { io.to(roomId).emit('game_state', game.state); } }); // 4. VOTAR EQUIPO socket.on('vote_team', ({ roomId, approve }) => { const game = games[roomId]; if (game) { game.voteForTeam(socket.id, approve); io.to(roomId).emit('game_state', game.state); } }); // 5. VOTAR MISIÓN socket.on('vote_mission', ({ roomId, success }) => { const game = games[roomId]; if (game) { game.voteMission(success); io.to(roomId).emit('game_state', game.state); } }); // 5.1 FINALIZAR REVELACIÓN DE CARTAS socket.on('finish_reveal', ({ roomId }) => { const game = games[roomId]; // Permitir a cualquiera avanzar para evitar bloqueos if (game && game.state.phase === 'mission_reveal') { game.finishReveal(); io.to(roomId).emit('game_state', game.state); } }); // 5.2 FINALIZAR PANTALLA DE RESULTADO socket.on('finish_mission_result', ({ roomId }) => { const game = games[roomId]; if (game && game.hostId === socket.id && game.state.phase === GamePhase.MISSION_RESULT) { game.finishMissionResult(); io.to(roomId).emit('game_state', game.state); // Si volvió a vote_leader (nueva ronda), iniciar timer // TypeScript no detecta que finishMissionResult() cambia la fase, usamos type assertion if ((game.state.phase as GamePhase) === GamePhase.VOTE_LEADER) { startLeaderVoteTimer(roomId); } } }); // 6. ASESINATO FINAL socket.on('assassin_kill', ({ roomId, targetId }) => { const game = games[roomId]; if (game) { game.assassinKill(targetId); io.to(roomId).emit('game_state', game.state); } }); // 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) { // Notificar a todos los jugadores que la partida ha sido finalizada io.to(roomId).emit('game_finalized'); // Eliminar la partida inmediatamente del registro delete games[roomId]; // Actualizar lista de salas para todos los clientes io.emit('rooms_list', getRoomsList()); // Desconectar a todos los jugadores de la sala io.in(roomId).socketsLeave(roomId); } }); // 9. DESCONEXIÓN socket.on('disconnect', () => { // Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos) console.log('Desconectado:', socket.id); // TODO: Eliminar de la partida si está en LOBBY para liberar hueco }); }); const getRoomsList = () => { return Object.values(games).map(g => ({ id: g.state.roomId, name: g.roomName, hostId: g.hostId, currentPlayers: g.state.players.length, maxPlayers: g.maxPlayers, isPrivate: !!g.password, status: g.state.phase === 'lobby' ? 'waiting' : 'playing' })); }; app.get('/', (req, res) => { res.send('Servidor de La Resistencia funcionando 🚀'); }); server.listen(port, () => { console.log(`[server]: Servidor corriendo en http://localhost:${port}`); });