17 Commits

Author SHA1 Message Date
Resistencia Dev
ae9e268467 chore: Sincronizar docker-compose_prod.yml con el sistema de variables de entorno
Some checks failed
CI/CD - Francia Ocupada (La Resistencia) / build-and-deploy (push) Failing after 6s
2025-12-22 17:55:12 +01:00
Resistencia Dev
2a9d89ed9e style: Hacer que el panel de jugadores aparezca plegado por defecto 2025-12-22 17:52:34 +01:00
Resistencia Dev
6e9f4512fb style: Hacer que el historial de misiones aparezca plegado por defecto 2025-12-22 17:50:08 +01:00
Resistencia Dev
12ac47e6c1 style: Ajustar posición del historial a 60px para alineación perfecta 2025-12-22 17:47:06 +01:00
Resistencia Dev
7895df4fd0 fix: Aumentar separación entre botones de salir e historial
- Cambiar posición del historial de top-[46px] a top-[56px]
- Evita solapamiento entre los botones
- Separación de ~12px entre ambos
2025-12-22 17:44:24 +01:00
Resistencia Dev
8835e780eb feat: Intercambiar posiciones de botones de historial y salir
- Botón de salir (casita verde) ahora en top-4 (arriba)
- Botón de historial movido a top-[46px] (debajo)
- Separación de 2px entre ambos botones
- Botones alineados verticalmente en la esquina superior derecha
2025-12-22 17:43:41 +01:00
Resistencia Dev
8dc01132e7 feat: Igualar estilo del botón de lobby al botón de historial
- Pegado a la derecha (right-0)
- Mismo tamaño (px-2 py-3, icono w-4 h-4)
- Gradiente verde (from-green-600 to-green-700)
- Borde redondeado solo a la izquierda (rounded-l-lg)
- Sin texto, solo icono de casita
- strokeWidth aumentado a 3 para mayor visibilidad
2025-12-22 17:41:36 +01:00
Resistencia Dev
6369421cb6 fix: Posicionar botón de lobby debajo del botón de historial
- Cambiar top-4 a top-20
- Ahora el botón verde de lobby está debajo del botón de historial
2025-12-22 17:38:58 +01:00
Resistencia Dev
060b604e04 feat: Mejorar diseño del botón de salir de la partida
- Mover botón de arriba-izquierda a arriba-derecha
- Cambiar color de amarillo a verde (bg-green-800)
- Cambiar icono de flecha por icono de casita (home)
- Cambiar texto de 'Salir' a 'Lobby'
- Actualizar tooltip a 'Volver al lobby'
2025-12-22 17:34:01 +01:00
Resistencia Dev
40c9de3388 fix: Configurar correctamente variables de entorno para acceso desde red local
- Agregar ARG en client/Dockerfile para NEXT_PUBLIC_API_URL
- Pasar build args en docker-compose.yml
- Asegurar que Next.js reciba la URL correcta del servidor
- Permitir acceso desde 192.168.1.131
2025-12-22 17:27:03 +01:00
Resistencia Dev
b1322e05c6 feat: Configurar acceso desde red local
- Crear archivo .env.example con configuración de red local
- Actualizar docker-compose.yml para usar variables de entorno
- Soportar acceso desde IP 192.168.1.131
- Documentar configuración en NETWORK-ACCESS.md
- Permitir acceso desde móviles y tablets en la misma red
2025-12-22 17:20:41 +01:00
Resistencia Dev
e9b4390db6 fix: Resolver bucle infinito de reconexiones
- Agregar bandera hasReconnected para ejecutar reconexión solo una vez
- Evita que el useEffect se ejecute infinitamente
- Reduce carga del servidor significativamente
2025-12-22 17:16:16 +01:00
Resistencia Dev
ef9d772441 revert: Restaurar tamaño original del botón de logout
- Padding vuelve a p-2
- Icono vuelve a w-5 h-5
- Tamaño más visible y usable
2025-12-22 17:13:23 +01:00
Resistencia Dev
5c7f52f793 fix: Reducir tamaño del botón de logout
- Padding reducido de p-2 a p-1.5
- Icono reducido de w-5 h-5 a w-4 h-4
- Botón más compacto y proporcional
2025-12-22 17:10:59 +01:00
Resistencia Dev
a6d1b11575 fix: Quitar posicionamiento fixed del botón de logout
- Ahora el botón se integra en el header del lobby
- Aparece al lado derecho del nombre del agente
- Ya no está arriba a la izquierda de forma fija
2025-12-22 17:09:17 +01:00
Resistencia Dev
1c03149bbd fix: Mejorar diseño del botón de logout
- Cambiar icono a power button (Heroicons)
- Mover botón debajo del nombre del agente en el lobby
- Hacer botón más compacto (circular)
- Mejorar layout del header del lobby
2025-12-22 17:08:07 +01:00
Resistencia Dev
53a5e3886e feat: Implementar sesiones persistentes y botones de salida
- Añadido sistema de sesiones con localStorage
- Nuevo hook useSessionStorage para manejar sesiones
- Botón de salir de la partida (ExitGameButton) en todas las pantallas del juego
- Botón de logout (LogoutButton) solo en el lobby
- Evento leave_game en servidor para cerrar partida cuando alguien sale
- Evento reconnect_session para reconectar jugadores después de recargar
- Actualizado GameBoard para incluir botón de salida
- Actualizado page.tsx para manejar sesiones y logout
2025-12-22 16:51:35 +01:00
15 changed files with 592 additions and 16 deletions

View File

@@ -0,0 +1,62 @@
---
description: Implementación de sesiones persistentes y botones de salida
---
# Implementación de Sesiones y Botones de Salida
## Objetivo
Implementar tres mejoras principales:
1. Sistema de sesiones persistentes
2. Botón de salir de la partida (en todas las pantallas de juego)
3. Botón de salir del juego (solo en lobby)
## Tareas
### 1. Sistema de Sesiones Persistentes
**Cliente:**
- Crear hook `useSessionStorage` para manejar sesiones
- Guardar en localStorage:
- `playerName` y `fullPlayerName`
- `currentView` (login/lobby/game)
- `roomId` si está en una partida
- Al cargar la app, verificar si hay sesión activa
- Si hay sesión, reconectar al servidor y recuperar estado
**Servidor:**
- Implementar evento `reconnect_session` para validar y recuperar estado
- Mantener mapping de `socketId` a `playerId` persistente
- Al reconectar, actualizar el socketId del jugador en la partida
### 2. Botón de Salir de la Partida
**Cliente:**
- Crear componente `ExitGameButton` con icono de flecha
- Posicionar arriba a la izquierda
- Mostrar en todas las fases del juego (no en lobby)
- Al hacer clic, emitir evento `leave_game`
**Servidor:**
- Implementar evento `leave_game`
- Notificar a todos los jugadores que alguien salió
- Eliminar la partida de la BD
- Devolver a todos al lobby
### 3. Botón de Salir del Juego
**Cliente:**
- Crear componente `LogoutButton` con icono de apagar
- Posicionar arriba a la izquierda solo en lobby
- Al hacer clic:
- Limpiar localStorage
- Volver a vista de login
- Desconectar socket si está en partida
## Orden de Implementación
1. Crear hooks y utilidades para sesiones
2. Implementar botón de salir del juego (logout)
3. Implementar botón de salir de la partida
4. Implementar lógica de reconexión en servidor
5. Integrar sistema de sesiones en el cliente
6. Pruebas

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# Configuración de red local
# Cambia esta IP a la IP de tu PC en la red local
HOST_IP=192.168.1.131
# URLs para desarrollo local
NEXT_PUBLIC_API_URL=http://192.168.1.131:4000
CORS_ORIGIN=http://192.168.1.131:3000
# Configuración de base de datos
DATABASE_URL=postgresql://postgres:password@db:5432/resistencia
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=resistencia

62
NETWORK-ACCESS.md Normal file
View File

@@ -0,0 +1,62 @@
# Configuración de Acceso desde Red Local
## Problema
Por defecto, la aplicación está configurada para usar `localhost`, lo que solo permite acceso desde el mismo PC. Para acceder desde otros dispositivos (móviles, tablets, etc.) en la misma red local, necesitas configurar la IP de tu PC.
## Solución
### 1. Obtener la IP de tu PC
```bash
ip -c a
```
Busca la IP en la interfaz de red activa (ej: `enp3s0`). En este caso: `192.168.1.131`
### 2. Configurar el archivo .env
Edita el archivo `.env` en la raíz del proyecto:
```env
# Cambia esta IP a la IP de tu PC
HOST_IP=192.168.1.131
NEXT_PUBLIC_API_URL=http://192.168.1.131:4000
CORS_ORIGIN=http://192.168.1.131:3000
```
### 3. Reiniciar Docker
```bash
docker compose down
docker compose up -d --build
```
### 4. Acceder desde otros dispositivos
Desde tu móvil, tablet u otro dispositivo en la misma red WiFi, abre el navegador y ve a:
```
http://192.168.1.131:3000
```
## Notas Importantes
- **Firewall**: Asegúrate de que tu firewall permita conexiones en los puertos 3000 y 4000
- **Misma red**: Todos los dispositivos deben estar en la misma red WiFi/LAN
- **IP dinámica**: Si tu IP cambia (DHCP), tendrás que actualizar el archivo `.env` y reiniciar Docker
## Verificar Firewall (Ubuntu/Linux)
```bash
# Ver estado del firewall
sudo ufw status
# Si está activo, permitir los puertos
sudo ufw allow 3000/tcp
sudo ufw allow 4000/tcp
```
## Para volver a localhost
Simplemente edita `.env` y cambia las URLs a `localhost`:
```env
NEXT_PUBLIC_API_URL=http://localhost:4000
CORS_ORIGIN=http://localhost:3000
```
Y reinicia Docker.

112
SESIONES-IMPLEMENTACION.md Normal file
View File

@@ -0,0 +1,112 @@
# Resumen de Implementación: Sesiones y Botones de Salida
## Cambios Realizados
### 1. Sistema de Sesiones Persistentes ✅
#### Cliente
- **Nuevo hook**: `client/src/hooks/useSessionStorage.ts`
- Maneja el almacenamiento y recuperación de sesiones en localStorage
- Guarda: playerName, fullPlayerName, currentView, roomId
- **Actualización de `page.tsx`**:
- Integrado hook `useSessionStorage`
- Al iniciar sesión, se guarda la sesión
- Al cargar la app, se restaura la sesión si existe
- Al cambiar de vista (lobby/game), se actualiza la sesión
- Función `handleLogout` para limpiar sesión
#### Servidor
- **Nuevo evento**: `reconnect_session`
- Permite a un jugador reconectarse a una partida existente
- Actualiza el socketId del jugador en la partida
- Envía el estado actualizado al jugador reconectado
### 2. Botón de Salir de la Partida ✅
#### Cliente
- **Nuevo componente**: `client/src/components/ExitGameButton.tsx`
- Botón con icono de flecha
- Posicionado arriba a la izquierda (fixed top-4 left-4)
- Modal de confirmación antes de salir
- Se muestra en todas las fases del juego excepto en pantallas de victoria
- **Actualización de `GameBoard.tsx`**:
- Agregado prop `fullPlayerName`
- Integrado `ExitGameButton`
- Llama a `actions.leaveGame()` al confirmar
- **Actualización de `useSocket.ts`**:
- Nueva acción: `leaveGame()`
- Nuevo listener: `player_left_game`
#### Servidor
- **Nuevo evento**: `leave_game`
- Notifica a todos los jugadores que alguien abandonó
- Elimina la partida de la base de datos
- Limpia timers asociados
- Actualiza la lista de salas
- Desconecta a todos los jugadores de la sala
### 3. Botón de Salir del Juego (Logout) ✅
#### Cliente
- **Nuevo componente**: `client/src/components/LogoutButton.tsx`
- Botón con icono de apagar
- Posicionado arriba a la izquierda (fixed top-4 left-4)
- Solo visible en el lobby
- **Actualización de `page.tsx`**:
- Función `handleLogout()`:
- Limpia la sesión de localStorage
- Vuelve a la vista de login
- Si está en una partida, llama a `leaveGame()`
## Flujo de Uso
### Sesiones Persistentes
1. Usuario se loguea → sesión guardada en localStorage
2. Usuario cierra navegador
3. Usuario vuelve a abrir → sesión restaurada automáticamente
4. Si estaba en una partida, intenta reconectar
### Salir de la Partida
1. Usuario hace clic en botón de flecha (arriba izquierda)
2. Aparece modal de confirmación
3. Al confirmar:
- Servidor notifica a todos: "Jugador X ha abandonado"
- Partida eliminada de la BD
- Todos vuelven al lobby
### Salir del Juego (Logout)
1. Usuario hace clic en botón de apagar (arriba izquierda, solo en lobby)
2. Sesión eliminada de localStorage
3. Vuelve a pantalla de login
## Archivos Modificados
### Nuevos Archivos
- `client/src/hooks/useSessionStorage.ts`
- `client/src/components/LogoutButton.tsx`
- `client/src/components/ExitGameButton.tsx`
- `.agent/workflows/sesiones-y-botones.md`
### Archivos Modificados
- `client/src/app/page.tsx`
- `client/src/components/GameBoard.tsx`
- `client/src/hooks/useSocket.ts`
- `server/src/index.ts`
## Próximos Pasos
1. **Probar la aplicación**:
- Verificar que las sesiones persisten correctamente
- Probar el botón de salir de la partida
- Probar el botón de logout
- Verificar reconexión después de recargar
2. **Posibles mejoras**:
- Agregar notificación toast cuando alguien abandona
- Mejorar el manejo de errores en reconexión
- Agregar timeout de sesión (expiración automática)
- Guardar más información en la sesión (configuración, preferencias)

View File

@@ -1,5 +1,8 @@
FROM node:20-alpine
# Build argument for API URL
ARG NEXT_PUBLIC_API_URL=http://localhost:4000
# Create app directory
WORKDIR /app

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -2,9 +2,11 @@
import { useState, useEffect } from 'react';
import { useSocket } from '../hooks/useSocket';
import { useSessionStorage } from '../hooks/useSessionStorage';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
import GameBoard from '../components/GameBoard';
import LogoutButton from '../components/LogoutButton';
import { GameRoom } from '../../../shared/types';
// Constantes de apellidos
@@ -21,6 +23,7 @@ type ViewState = 'login' | 'lobby' | 'game';
export default function Home() {
const { isConnected, gameState, roomsList, actions, socket } = useSocket();
const { session, saveSession, updateSession, clearSession } = useSessionStorage();
// Estados locales de UI
const [view, setView] = useState<ViewState>('login');
@@ -34,14 +37,36 @@ export default function Home() {
const [passwordPromptRoomId, setPasswordPromptRoomId] = useState<string | null>(null);
const [joinPassword, setJoinPassword] = useState('');
const [hasReconnected, setHasReconnected] = useState(false);
// Restaurar sesión al cargar - SOLO UNA VEZ
useEffect(() => {
if (session && isConnected && !hasReconnected) {
setPlayerName(session.playerName);
setFullPlayerName(session.fullPlayerName);
setView(session.currentView);
// Si había una partida activa, intentar reconectar
if (session.roomId && session.currentView === 'game') {
actions.reconnectSession({ playerName: session.fullPlayerName, roomId: session.roomId });
} else if (session.currentView === 'lobby') {
actions.refreshRooms();
}
setHasReconnected(true);
}
}, [session, isConnected, hasReconnected]);
// Efecto para cambiar a vista de juego cuando el servidor nos une
useEffect(() => {
if (gameState?.roomId) {
setView('game');
// Guardar en sesión
updateSession({ currentView: 'game', roomId: gameState.roomId });
} else if (view === 'game' && !gameState) {
// Si estábamos en juego y volvemos a null, volver al lobby
setView('lobby');
updateSession({ currentView: 'lobby', roomId: undefined });
}
}, [gameState]);
@@ -50,13 +75,32 @@ export default function Home() {
if (playerName) {
// Generar apellido aleatorio
const randomSurname = SURNAMES[Math.floor(Math.random() * SURNAMES.length)];
setFullPlayerName(`${playerName} ${randomSurname}`);
const fullName = `${playerName} ${randomSurname}`;
setFullPlayerName(fullName);
// Guardar sesión
saveSession({
playerName,
fullPlayerName: fullName,
currentView: 'lobby'
});
setView('lobby');
actions.refreshRooms();
}
};
const handleLogout = () => {
clearSession();
setView('login');
setPlayerName('');
setFullPlayerName('');
// Si está en una partida, salir
if (gameState?.roomId) {
actions.leaveGame();
}
};
const handleCreateGame = (e: React.FormEvent) => {
e.preventDefault();
actions.createGame(fullPlayerName, createConfig.maxPlayers, createConfig.password);
@@ -149,8 +193,9 @@ export default function Home() {
return (
<GameBoard
gameState={gameState}
currentPlayerId={socket.id}
currentPlayerId={socket.id || ''}
actions={actions}
fullPlayerName={fullPlayerName}
/>
);
}
@@ -178,9 +223,12 @@ export default function Home() {
</h1>
</div>
{view === 'lobby' && (
<div className="flex items-center gap-4 bg-black/50 px-4 py-2 rounded border border-white/10">
<span className="text-sm text-gray-400">AGENTE:</span>
<span className="font-bold text-yellow-500">{fullPlayerName}</span>
<div className="flex items-center gap-3 bg-black/50 px-4 py-2 rounded border border-white/10">
<div className="flex flex-col">
<span className="text-xs text-gray-400">AGENTE:</span>
<span className="font-bold text-yellow-500">{fullPlayerName}</span>
</div>
<LogoutButton onClick={handleLogout} />
</div>
)}
</div>

View File

@@ -0,0 +1,79 @@
import { motion } from 'framer-motion';
import { useState } from 'react';
interface ExitGameButtonProps {
onExit: () => void;
playerName: string;
}
export default function ExitGameButton({ onExit, playerName }: ExitGameButtonProps) {
const [showConfirm, setShowConfirm] = useState(false);
const handleConfirmExit = () => {
setShowConfirm(false);
onExit();
};
return (
<>
<motion.button
onClick={() => setShowConfirm(true)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="fixed top-4 right-0 z-50 bg-gradient-to-l from-green-600 to-green-700 hover:from-green-500 hover:to-green-600 text-white rounded-l-lg px-2 py-3 shadow-lg border-2 border-green-500 border-r-0 transition-all hover:shadow-green-500/50"
title="Volver al lobby"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={3}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
</motion.button>
{/* Modal de confirmación */}
{showConfirm && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="bg-zinc-900 p-6 rounded-lg border border-red-700/50 w-full max-w-md mx-4 shadow-2xl"
>
<h3 className="text-xl font-bold text-red-400 mb-4 uppercase flex items-center gap-2">
Abandonar Partida
</h3>
<p className="text-gray-300 mb-2">
¿Estás seguro de que quieres abandonar la partida?
</p>
<p className="text-sm text-gray-400 mb-6">
La partida se cerrará para todos los jugadores y se perderá todo el progreso.
</p>
<div className="flex gap-3">
<button
onClick={() => setShowConfirm(false)}
className="flex-1 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded font-bold uppercase text-sm transition-colors"
>
Cancelar
</button>
<button
onClick={handleConfirmExit}
className="flex-1 py-3 bg-red-900 hover:bg-red-800 text-white rounded font-bold uppercase text-sm transition-colors"
>
Salir
</button>
</div>
</motion.div>
</div>
)}
</>
);
}

View File

@@ -5,14 +5,16 @@ import { GameState, GamePhase, Player, GAME_CONFIG, Faction } from '../../../sha
import MissionReveal from './MissionReveal';
import MissionResult from './MissionResult';
import VictoryScreen from './VictoryScreen';
import ExitGameButton from './ExitGameButton';
interface GameBoardProps {
gameState: GameState;
currentPlayerId: string;
actions: any;
fullPlayerName: string;
}
export default function GameBoard({ gameState, currentPlayerId, actions }: GameBoardProps) {
export default function GameBoard({ gameState, currentPlayerId, actions, fullPlayerName }: GameBoardProps) {
const [selectedTeam, setSelectedTeam] = useState<string[]>([]);
// Hooks para FASE REVEAL ROLE
@@ -26,10 +28,10 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
const [expandedMission, setExpandedMission] = useState<number | null>(null);
// Estado para controlar el colapso del panel de jugadores
const [isPlayersCollapsed, setIsPlayersCollapsed] = useState(false);
const [isPlayersCollapsed, setIsPlayersCollapsed] = useState(true);
// Estado para controlar el colapso del historial de misiones
const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(false);
const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(true);
// Timer para avanzar automáticamente en REVEAL_ROLE
@@ -298,6 +300,14 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
return (
<div className="relative w-full h-screen flex flex-col overflow-hidden">
{/* Botón de Salir de la Partida - No mostrar en pantallas de victoria */}
{gameState.phase !== GamePhase.ALLIED_WIN && gameState.phase !== GamePhase.NAZIS_WIN && (
<ExitGameButton
onExit={() => actions.leaveGame()}
playerName={fullPlayerName}
/>
)}
{/* Fondo */}
<div className="absolute inset-0 z-0 opacity-40">
<Image
@@ -887,7 +897,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
{/* HISTÓRICO DE MISIONES (Esquina superior derecha) */}
{gameState.missionHistory.length > 0 && (
<motion.div
className="fixed top-4 right-0 z-50"
className="fixed top-[60px] right-0 z-50"
initial={false}
animate={{
x: isHistoryCollapsed ? '0%' : '0%'

View File

@@ -0,0 +1,32 @@
import { motion } from 'framer-motion';
interface LogoutButtonProps {
onClick: () => void;
}
export default function LogoutButton({ onClick }: LogoutButtonProps) {
return (
<motion.button
onClick={onClick}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="bg-red-900/80 hover:bg-red-800 text-white p-2 rounded-full border border-red-700/50 backdrop-blur-sm shadow-lg transition-all"
title="Salir del juego"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.636 5.636a9 9 0 1012.728 0M12 3v9"
/>
</svg>
</motion.button>
);
}

View File

@@ -0,0 +1,53 @@
import { useState, useEffect } from 'react';
interface SessionData {
playerName: string;
fullPlayerName: string;
currentView: 'login' | 'lobby' | 'game';
roomId?: string;
}
export function useSessionStorage() {
const [session, setSession] = useState<SessionData | null>(null);
// Cargar sesión al iniciar
useEffect(() => {
const savedSession = localStorage.getItem('resistencia_session');
if (savedSession) {
try {
const parsed = JSON.parse(savedSession);
setSession(parsed);
} catch (e) {
console.error('Error parsing session:', e);
localStorage.removeItem('resistencia_session');
}
}
}, []);
// Guardar sesión
const saveSession = (data: SessionData) => {
localStorage.setItem('resistencia_session', JSON.stringify(data));
setSession(data);
};
// Actualizar sesión parcialmente
const updateSession = (partial: Partial<SessionData>) => {
if (session) {
const updated = { ...session, ...partial };
saveSession(updated);
}
};
// Limpiar sesión
const clearSession = () => {
localStorage.removeItem('resistencia_session');
setSession(null);
};
return {
session,
saveSession,
updateSession,
clearSession
};
}

View File

@@ -50,6 +50,12 @@ export const useSocket = () => {
setGameState(null); // Resetear estado para volver al lobby
});
// Manejar cuando un jugador abandona la partida
socketInstance.on('player_left_game', ({ playerName }: { playerName: string }) => {
console.log(`${playerName} ha abandonado la partida`);
// El servidor ya habrá cerrado la partida, solo mostramos mensaje
});
setSocket(socketInstance);
return () => {
@@ -90,6 +96,14 @@ export const useSocket = () => {
socket?.emit('assassin_kill', { roomId: gameState?.roomId, targetId });
};
const leaveGame = () => {
socket?.emit('leave_game', { roomId: gameState?.roomId });
};
const reconnectSession = (sessionData: { playerName: string; roomId?: string }) => {
socket?.emit('reconnect_session', sessionData);
};
return {
socket,
isConnected,
@@ -105,6 +119,8 @@ export const useSocket = () => {
voteMission,
voteLeader: (approve: boolean) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
assassinKill,
leaveGame,
reconnectSession,
finishIntro: () => socket?.emit('finish_intro', { roomId: gameState?.roomId }),
finishReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }),
finishRollCall: () => socket?.emit('finish_roll_call', { roomId: gameState?.roomId }),

View File

@@ -5,6 +5,8 @@ services:
build:
context: .
dockerfile: client/Dockerfile
args:
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:4000}
ports:
- "3000:3000"
volumes:
@@ -12,7 +14,7 @@ services:
- ./shared:/app/shared
- /app/client/node_modules
environment:
- NEXT_PUBLIC_API_URL=http://localhost:4000
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:4000}
depends_on:
- server
networks:
@@ -32,8 +34,8 @@ services:
- /app/server/node_modules
environment:
- PORT=4000
- DATABASE_URL=postgresql://postgres:password@db:5432/resistencia
- CORS_ORIGIN=http://localhost:3000
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:password@db:5432/resistencia}
- CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:3000}
depends_on:
- db
networks:

View File

@@ -5,6 +5,8 @@ services:
build:
context: .
dockerfile: client/Dockerfile
args:
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-https://api.franciaocupada.martivich.es}
ports:
- "3000:3000"
volumes:
@@ -12,7 +14,7 @@ services:
- ./shared:/app/shared
- /app/client/node_modules
environment:
- NEXT_PUBLIC_API_URL=https://api.franciaocupada.martivich.es
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-https://api.franciaocupada.martivich.es}
depends_on:
- server
networks:
@@ -32,8 +34,8 @@ services:
- /app/server/node_modules
environment:
- PORT=4000
- DATABASE_URL=postgresql://postgres:password@db:5432/resistencia
- CORS_ORIGIN=https://franciaocupada.martivich.es
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:password@db:5432/resistencia}
- CORS_ORIGIN=${CORS_ORIGIN:-https://franciaocupada.martivich.es}
depends_on:
- db
networks:

View File

@@ -301,7 +301,89 @@ io.on('connection', (socket) => {
}
});
// 9. DESCONEXIÓN
// 8. ABANDONAR PARTIDA (cualquier jugador)
socket.on('leave_game', ({ roomId }) => {
const game = games[roomId];
if (game) {
// Encontrar el jugador que se va
const leavingPlayer = game.state.players.find(p => p.id === socket.id);
const playerName = leavingPlayer?.name || 'Un jugador';
// Notificar a todos los jugadores
io.to(roomId).emit('player_left_game', { playerName });
io.to(roomId).emit('game_finalized');
// Eliminar la partida de la base de datos
delete games[roomId];
// Limpiar timer si existe
if (voteTimers[roomId]) {
clearTimeout(voteTimers[roomId]);
delete voteTimers[roomId];
}
// Actualizar lista de salas
io.emit('rooms_list', getRoomsList());
// Desconectar a todos de la sala
io.in(roomId).socketsLeave(roomId);
console.log(`[LEAVE_GAME] ${playerName} abandonó la partida ${roomId}. Partida eliminada.`);
}
});
// 9. RECONECTAR SESIÓN
socket.on('reconnect_session', ({ playerName, roomId }) => {
console.log(`[RECONNECT_SESSION] Intento de reconexión: ${playerName} a sala ${roomId}`);
if (roomId) {
const game = games[roomId];
if (game) {
// Buscar si el jugador existe en la partida
const existingPlayer = game.state.players.find(p => p.name === playerName);
if (existingPlayer) {
// Actualizar el socket ID del jugador
existingPlayer.id = socket.id;
// Unir al socket a la sala
socket.join(roomId);
// Enviar estado actualizado
socket.emit('game_joined', { roomId, state: game.state });
io.to(roomId).emit('game_state', game.state);
console.log(`[RECONNECT_SESSION] ${playerName} reconectado exitosamente a ${roomId}`);
} else {
console.log(`[RECONNECT_SESSION] Jugador ${playerName} no encontrado en partida ${roomId}`);
socket.emit('error', 'No se pudo reconectar a la partida');
}
} else {
console.log(`[RECONNECT_SESSION] Partida ${roomId} no existe`);
socket.emit('error', 'La partida ya no existe');
}
}
});
// 10. FINALIZAR Y EXPULSAR JUGADORES (solo host)
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);
}
});
// 11. DESCONEXIÓN
socket.on('disconnect', () => {
// Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos)
console.log('Desconectado:', socket.id);