Release v1.0 - Primera versión estable de Francia Ocupada
- Implementación completa del juego La Resistencia - Sistema de roles: Aliados y Nazis (incluyendo Francotirador) - Fases del juego: Selección de equipo, votación, misión, asesinato - Interfaz de usuario con imágenes temáticas - Sistema de WebSockets para multijugador en tiempo real - Configuración Docker para desarrollo y producción - Dockerfiles optimizados para cliente y servidor - docker-compose.yml para desarrollo local - docker-compose_prod.yml para despliegue en producción con Nginx Proxy Manager - Base de datos PostgreSQL integrada - Documentación de cambios y fases del juego
This commit is contained in:
171
CAMBIOS_SESION_2025-12-08.txt
Normal file
171
CAMBIOS_SESION_2025-12-08.txt
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
RESUMEN DE CAMBIOS - SESIÓN 2025-12-08
|
||||||
|
========================================
|
||||||
|
|
||||||
|
## 1. CORRECCIÓN DE PANTALLAS DE VICTORIA (ALLIED_WIN y NAZIS_WIN)
|
||||||
|
|
||||||
|
### Problema inicial:
|
||||||
|
- Las pantallas de victoria mostraban el tablero de juego encima de la imagen de fondo
|
||||||
|
- Había imágenes duplicadas y rutas incorrectas (.jpg vs .png)
|
||||||
|
|
||||||
|
### Solución implementada:
|
||||||
|
|
||||||
|
#### GameBoard.tsx:
|
||||||
|
- **Fondo dinámico según fase**: El fondo del componente GameBoard ahora cambia según la fase:
|
||||||
|
* ALLIED_WIN → muestra `/assets/images/tokens/mission_success.png`
|
||||||
|
* NAZIS_WIN → muestra `/assets/images/tokens/mission_fail.png`
|
||||||
|
* Otras fases → muestra `/assets/images/ui/bg_game.png`
|
||||||
|
|
||||||
|
- **Área del tablero oculta en victorias**: El div del tablero (con las cartas de misión, tablero táctico, etc.)
|
||||||
|
se oculta completamente cuando `gameState.phase === ALLIED_WIN || gameState.phase === NAZIS_WIN`
|
||||||
|
|
||||||
|
#### VictoryScreen.tsx:
|
||||||
|
- **Eliminada imagen de fondo redundante**: Se eliminó el div con la imagen de fondo que intentaba cargar
|
||||||
|
`mission_fail.jpg` y `mission_success.jpg`, ya que el GameBoard ahora maneja estos fondos.
|
||||||
|
|
||||||
|
### Archivos modificados:
|
||||||
|
- `client/src/components/GameBoard.tsx` (líneas 293-307, 309-442)
|
||||||
|
- `client/src/components/VictoryScreen.tsx` (líneas 39-50 eliminadas)
|
||||||
|
|
||||||
|
### Commit:
|
||||||
|
- Hash: 6e65152
|
||||||
|
- Mensaje: "feat: Fix victory screens background images"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. MEJORA DE CARTAS DE MISIÓN (Fase MISSION)
|
||||||
|
|
||||||
|
### Problema inicial:
|
||||||
|
- Las cartas solo se opacaban cuando se seleccionaba la otra
|
||||||
|
- Si solo había una carta (jugadores aliados), no había feedback visual de que se había seleccionado
|
||||||
|
|
||||||
|
### Solución implementada:
|
||||||
|
|
||||||
|
#### Cambio de lógica de opacidad:
|
||||||
|
**ANTES:**
|
||||||
|
- Sin voto: todas las cartas al 100% de opacidad
|
||||||
|
- Con voto: la carta NO seleccionada se opaca al 50%
|
||||||
|
|
||||||
|
**DESPUÉS:**
|
||||||
|
- Sin voto: todas las cartas al 50% de opacidad (opacadas por defecto)
|
||||||
|
- Con voto: solo la carta seleccionada se pone al 100%, las demás permanecen al 50%
|
||||||
|
|
||||||
|
#### Implementación:
|
||||||
|
```tsx
|
||||||
|
// Carta de Éxito
|
||||||
|
className={`group transition-opacity ${missionVote === true ? 'opacity-100' : 'opacity-50'}`}
|
||||||
|
|
||||||
|
// Carta de Sabotaje (solo alemanes)
|
||||||
|
className={`group transition-opacity ${missionVote === false ? 'opacity-100' : 'opacity-50'}`}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Archivos modificados:
|
||||||
|
- `client/src/components/GameBoard.tsx` (líneas 628-678)
|
||||||
|
|
||||||
|
### Beneficio:
|
||||||
|
- Ahora es fácil ver qué carta has seleccionado, incluso cuando solo tienes una opción disponible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. INTENTO DE MEJORA DEL HISTORIAL DE MISIONES (NO FUNCIONAL)
|
||||||
|
|
||||||
|
### Objetivo:
|
||||||
|
- Mostrar los participantes de cada misión al hacer clic en el número del historial
|
||||||
|
|
||||||
|
### Implementación intentada:
|
||||||
|
|
||||||
|
#### Estado añadido:
|
||||||
|
```tsx
|
||||||
|
const [expandedMission, setExpandedMission] = useState<number | null>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Lógica implementada:
|
||||||
|
- Click en número de misión → expande mostrando nombres de participantes
|
||||||
|
- Click de nuevo → colapsa la lista
|
||||||
|
- Solo una misión puede estar expandida a la vez
|
||||||
|
- Indicador visual: anillo amarillo alrededor del número cuando está expandido
|
||||||
|
|
||||||
|
#### Código añadido en GameBoard.tsx (líneas 856-899):
|
||||||
|
```tsx
|
||||||
|
{gameState.missionHistory.map((mission, idx) => {
|
||||||
|
const isExpanded = expandedMission === idx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className="relative">
|
||||||
|
<div
|
||||||
|
className={`... ${isExpanded ? 'ring-2 ring-yellow-400' : ''}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('Click en misión', idx, 'Estado actual:', expandedMission);
|
||||||
|
setExpandedMission(isExpanded ? null : idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mission.round}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="absolute top-10 right-0 bg-black/95 p-2 rounded border border-white/30 min-w-max z-[100]">
|
||||||
|
{mission.team.map((playerId) => {
|
||||||
|
const player = gameState.players.find(p => p.id === playerId);
|
||||||
|
return (
|
||||||
|
<div key={playerId} className="text-xs text-white whitespace-nowrap">
|
||||||
|
{player?.name || playerId}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Archivos modificados:
|
||||||
|
- `client/src/components/GameBoard.tsx` (líneas 26, 856-899)
|
||||||
|
|
||||||
|
### Estado:
|
||||||
|
⚠️ **NO FUNCIONAL** - El click no dispara la expansión de la lista de participantes.
|
||||||
|
Posibles causas a investigar:
|
||||||
|
- Conflicto con otros event handlers
|
||||||
|
- Problema con el z-index o posicionamiento
|
||||||
|
- Estado no actualizándose correctamente
|
||||||
|
- Necesidad de reiniciar servicios Docker
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RESUMEN DE COMMITS
|
||||||
|
|
||||||
|
1. **6e65152** - "feat: Fix victory screens background images"
|
||||||
|
- Corregidas pantallas de victoria
|
||||||
|
- Eliminadas imágenes redundantes
|
||||||
|
- Fondo dinámico según fase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ARCHIVOS PRINCIPALES MODIFICADOS
|
||||||
|
|
||||||
|
1. `client/src/components/GameBoard.tsx`
|
||||||
|
- Fondo dinámico para fases de victoria
|
||||||
|
- Área del tablero oculta en victorias
|
||||||
|
- Opacidad de cartas de misión mejorada
|
||||||
|
- Intento de historial expandible (no funcional)
|
||||||
|
|
||||||
|
2. `client/src/components/VictoryScreen.tsx`
|
||||||
|
- Eliminada imagen de fondo redundante
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PENDIENTES / PROBLEMAS CONOCIDOS
|
||||||
|
|
||||||
|
1. ❌ **Historial de misiones expandible no funciona**
|
||||||
|
- El código está implementado pero el click no dispara la acción
|
||||||
|
- Requiere investigación adicional
|
||||||
|
|
||||||
|
2. ⚠️ **Errores de lint**
|
||||||
|
- Múltiples errores de tipo "JSX element implicitly has type 'any'"
|
||||||
|
- Son falsos positivos del IDE en entorno Dockerizado
|
||||||
|
- No afectan la funcionalidad de la aplicación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Fecha: 2025-12-08
|
||||||
|
Hora: 22:59
|
||||||
@@ -23,6 +23,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
|
|
||||||
// Track del voto de misión del jugador
|
// Track del voto de misión del jugador
|
||||||
const [missionVote, setMissionVote] = useState<boolean | null>(null);
|
const [missionVote, setMissionVote] = useState<boolean | null>(null);
|
||||||
|
const [expandedMission, setExpandedMission] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|
||||||
// Timer para avanzar automáticamente en REVEAL_ROLE
|
// Timer para avanzar automáticamente en REVEAL_ROLE
|
||||||
@@ -628,7 +629,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
{/* Carta de Éxito primero */}
|
{/* Carta de Éxito primero */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMissionVote(true)}
|
onClick={() => handleMissionVote(true)}
|
||||||
className={`group transition-opacity ${missionVote !== null && missionVote !== true ? 'opacity-50' : 'opacity-100'}`}
|
className={`group transition-opacity ${missionVote === true ? 'opacity-100' : 'opacity-50'}`}
|
||||||
disabled={missionVote !== null}
|
disabled={missionVote !== null}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -643,7 +644,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
|
|
||||||
{/* Carta de Sabotaje segundo (solo para alemanes) */}
|
{/* Carta de Sabotaje segundo (solo para alemanes) */}
|
||||||
{currentPlayer?.faction === Faction.ALEMANES && (
|
{currentPlayer?.faction === Faction.ALEMANES && (
|
||||||
<button onClick={() => handleMissionVote(false)} className="group transition-opacity" style={{ opacity: missionVote !== null && missionVote !== false ? 0.5 : 1 }} disabled={missionVote !== null}>
|
<button
|
||||||
|
onClick={() => handleMissionVote(false)}
|
||||||
|
className={`group transition-opacity ${missionVote === false ? 'opacity-100' : 'opacity-50'}`}
|
||||||
|
disabled={missionVote !== null}
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-64 h-96 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
className="w-64 h-96 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
||||||
whileHover={{ scale: 1.1, rotate: -3 }}
|
whileHover={{ scale: 1.1, rotate: -3 }}
|
||||||
@@ -659,7 +664,11 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
<>
|
<>
|
||||||
{/* Carta de Sabotaje primero (solo para alemanes) */}
|
{/* Carta de Sabotaje primero (solo para alemanes) */}
|
||||||
{currentPlayer?.faction === Faction.ALEMANES && (
|
{currentPlayer?.faction === Faction.ALEMANES && (
|
||||||
<button onClick={() => handleMissionVote(false)} className="group transition-opacity" style={{ opacity: missionVote !== null && missionVote !== false ? 0.5 : 1 }} disabled={missionVote !== null}>
|
<button
|
||||||
|
onClick={() => handleMissionVote(false)}
|
||||||
|
className={`group transition-opacity ${missionVote === false ? 'opacity-100' : 'opacity-50'}`}
|
||||||
|
disabled={missionVote !== null}
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-64 h-96 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
className="w-64 h-96 bg-gradient-to-br from-red-600 to-red-900 rounded-2xl shadow-2xl border-4 border-red-400 flex flex-col items-center justify-center p-6 transform transition-all hover:scale-110 hover:-rotate-3 hover:shadow-red-500/50"
|
||||||
whileHover={{ scale: 1.1, rotate: -3 }}
|
whileHover={{ scale: 1.1, rotate: -3 }}
|
||||||
@@ -674,7 +683,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
{/* Carta de Éxito segundo */}
|
{/* Carta de Éxito segundo */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMissionVote(true)}
|
onClick={() => handleMissionVote(true)}
|
||||||
className={`group transition-opacity ${missionVote !== null && missionVote !== true ? 'opacity-50' : 'opacity-100'}`}
|
className={`group transition-opacity ${missionVote === true ? 'opacity-100' : 'opacity-50'}`}
|
||||||
disabled={missionVote !== null}
|
disabled={missionVote !== null}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -848,18 +857,42 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
<div className="absolute top-4 right-4 bg-black/80 p-3 rounded-lg border border-white/20 backdrop-blur-sm">
|
<div className="absolute top-4 right-4 bg-black/80 p-3 rounded-lg border border-white/20 backdrop-blur-sm">
|
||||||
<div className="text-[10px] text-gray-400 uppercase mb-2 text-center font-bold tracking-wider">Historial</div>
|
<div className="text-[10px] text-gray-400 uppercase mb-2 text-center font-bold tracking-wider">Historial</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{gameState.missionHistory.map((mission, idx) => (
|
{gameState.missionHistory.map((mission, idx) => {
|
||||||
|
const isExpanded = expandedMission === idx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className="relative">
|
||||||
<div
|
<div
|
||||||
key={idx}
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold border-2 cursor-pointer transition-all hover:scale-110 ${mission.isSuccess
|
||||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold border-2 ${mission.isSuccess
|
|
||||||
? 'bg-blue-600 border-blue-400 text-white'
|
? 'bg-blue-600 border-blue-400 text-white'
|
||||||
: 'bg-red-600 border-red-400 text-white'
|
: 'bg-red-600 border-red-400 text-white'
|
||||||
}`}
|
} ${isExpanded ? 'ring-2 ring-yellow-400' : ''}`}
|
||||||
title={`Misión ${mission.round}: ${mission.isSuccess ? 'Éxito' : 'Fracaso'} (${mission.successes}✓ ${mission.fails}✗)`}
|
title={`Misión ${mission.round}: ${mission.isSuccess ? 'Éxito' : 'Fracaso'} (${mission.successes}✓ ${mission.fails}✗)`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('Click en misión', idx, 'Estado actual:', expandedMission);
|
||||||
|
setExpandedMission(isExpanded ? null : idx);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{mission.round}
|
{mission.round}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
{/* Lista de participantes */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="absolute top-10 right-0 bg-black/95 p-2 rounded border border-white/30 min-w-max z-[100]">
|
||||||
|
{mission.team.map((playerId) => {
|
||||||
|
const player = gameState.players.find(p => p.id === playerId);
|
||||||
|
return (
|
||||||
|
<div key={playerId} className="text-xs text-white whitespace-nowrap">
|
||||||
|
{player?.name || playerId}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
63
docker-compose_prod.yml
Normal file
63
docker-compose_prod.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
services:
|
||||||
|
# --- FRONTEND (Next.js) ---
|
||||||
|
client:
|
||||||
|
container_name: resistencia-client
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: client/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./client:/app/client
|
||||||
|
- ./shared:/app/shared
|
||||||
|
- /app/client/node_modules
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=https://api.franciaocupada.martivich.es
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
networks:
|
||||||
|
- resistencia-net
|
||||||
|
|
||||||
|
# --- BACKEND (Node/Express + Socket.io) ---
|
||||||
|
server:
|
||||||
|
container_name: resistencia-server
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: server/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
volumes:
|
||||||
|
- ./server:/app/server
|
||||||
|
- ./shared:/app/shared
|
||||||
|
- /app/server/node_modules
|
||||||
|
environment:
|
||||||
|
- PORT=4000
|
||||||
|
- DATABASE_URL=postgresql://postgres:password@db:5432/resistencia
|
||||||
|
- CORS_ORIGIN=https://franciaocupada.martivich.es
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- resistencia-net
|
||||||
|
|
||||||
|
# --- BASE DE DATOS (PostgreSQL) ---
|
||||||
|
db:
|
||||||
|
container_name: resistencia-db
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: resistencia
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- resistencia-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
resistencia-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
Reference in New Issue
Block a user