Files
FranciaOcupada/client/src/app/page.tsx
Resistencia Dev 9e0e343868 feat: Actualizar roles y facciones a Francia Ocupada
- Cambiar nombre del juego de 'La Resistencia' a 'Francia Ocupada'
- Actualizar roles: Marlene, Capitán Philippe, Partisano, Comandante Schmidt, Francotirador, Agente Doble, Infiltrado, Colaboracionista
- Actualizar facciones: Aliados vs Alemanes
- Implementar timer de votación de líder con auto-avance
- Eliminar componentes de debug
2025-12-07 00:20:33 +01:00

388 lines
21 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useSocket } from '../hooks/useSocket';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
import GameBoard from '../components/GameBoard';
import { GameRoom } from '../../../shared/types';
// Constantes de apellidos
const SURNAMES = [
// Franceses
'Dubois', 'Leroy', 'Moreau', 'Petit', 'Lefebvre', 'Michel', 'Durand',
// Británicos
'Smith', 'Jones', 'Williams', 'Brown', 'Taylor', 'Wilson', 'Evans',
// Americanos
'Miller', 'Davis', 'Garcia', 'Rodriguez', 'Martinez', 'Hernandez'
];
type ViewState = 'login' | 'lobby' | 'game';
export default function Home() {
const { isConnected, gameState, roomsList, actions, socket } = useSocket();
// Estados locales de UI
const [view, setView] = useState<ViewState>('login');
const [playerName, setPlayerName] = useState('');
// El apellido se genera al loguearse
const [fullPlayerName, setFullPlayerName] = useState('');
// UI Create/Join
const [showCreateModal, setShowCreateModal] = useState(false);
const [createConfig, setCreateConfig] = useState({ maxPlayers: 5, password: '' });
const [passwordPromptRoomId, setPasswordPromptRoomId] = useState<string | null>(null);
const [joinPassword, setJoinPassword] = useState('');
// Efecto para cambiar a vista de juego cuando el servidor nos une
useEffect(() => {
if (gameState?.roomId) {
setView('game');
} else if (view === 'game' && !gameState) {
// Si estábamos en juego y volvemos a null, volver al lobby
setView('lobby');
}
}, [gameState]);
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (playerName) {
// Generar apellido aleatorio
const randomSurname = SURNAMES[Math.floor(Math.random() * SURNAMES.length)];
setFullPlayerName(`${playerName} ${randomSurname}`);
setView('lobby');
actions.refreshRooms();
}
};
const handleCreateGame = (e: React.FormEvent) => {
e.preventDefault();
actions.createGame(fullPlayerName, createConfig.maxPlayers, createConfig.password);
setShowCreateModal(false);
};
const requestJoinGame = (room: GameRoom) => {
if (room.isPrivate) {
setPasswordPromptRoomId(room.id);
setJoinPassword('');
} else {
actions.joinGame(room.id, fullPlayerName);
}
};
const submitJoinPassword = () => {
if (passwordPromptRoomId) {
actions.joinGame(passwordPromptRoomId, fullPlayerName, joinPassword);
setPasswordPromptRoomId(null);
}
};
// --- RENDER DE JUEGO O SALA DE ESPERA ---
if (view === 'game' && gameState && socket) {
// ¿Estamos en fase de lobby dentro de la partida?
if (gameState.phase === 'lobby') {
const isHost = gameState.hostId === socket.id;
// Podríamos obtener maxPlayers del array players si no lo tenemos en gameState,
// pero lo ideal sería tenerlo. Por ahora asumimos que si ya estamos dentro,
// sabemos cuantos somos.
// NOTA: GameState no tiene maxPlayers explicitamente, pero podemos deducirlo o añadirlo.
// Por simplicidad, usaremos el length para validar minimo.
return (
<main className="relative min-h-screen flex flex-col items-center justify-center overflow-hidden bg-zinc-900 font-mono text-gray-200">
<div className="absolute inset-0 z-0 opacity-40">
<Image src="/assets/images/ui/bg_game.png" alt="War Room" fill className="object-cover" />
<div className="absolute inset-0 bg-black/70" />
</div>
<div className="z-10 bg-black/80 p-8 rounded border border-white/20 max-w-2xl w-full mx-4 backdrop-blur-md">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-yellow-500 mb-2 uppercase tracking-widest">Sala de Espera</h2>
<p className="text-gray-400">Operación en curso. Esperando activación...</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-8">
{gameState.players.map(player => (
<div key={player.id} className="bg-white/5 p-3 rounded flex items-center gap-3 border border-white/10">
<div className={`w-3 h-3 rounded-full ${player.id === socket.id ? 'bg-green-500 shadow-green-500/50 shadow-lg' : 'bg-gray-500'}`} />
<span className={player.id === socket.id ? 'font-bold text-white' : 'text-gray-300'}>
{player.name}
</span>
{player.id === gameState.hostId && (
<span className="text-[10px] bg-yellow-900/50 text-yellow-500 px-2 py-0.5 rounded ml-auto">HOST</span>
)}
</div>
))}
{/* Rellenar huecos vacíos visualmente si quisieramos */}
</div>
<div className="flex flex-col items-center gap-4 border-t border-white/10 pt-6">
{isHost ? (
<>
<p className="text-sm text-gray-400 mb-2">
Jugadores: <span className="text-white font-bold">{gameState.players.length}</span>
{gameState.players.length < 5 && <span className="text-red-400 ml-2">(Mínimo 5 requeridos)</span>}
</p>
<button
onClick={() => actions.startGame()}
disabled={gameState.players.length < 5}
className="w-full max-w-md bg-yellow-600 hover:bg-yellow-500 disabled:bg-gray-700 disabled:cursor-not-allowed text-white font-bold py-4 rounded uppercase tracking-[0.2em] transition-all shadow-lg"
>
INICIAR MISIÓN
</button>
</>
) : (
<div className="text-center animate-pulse">
<p className="text-yellow-500 font-bold uppercase tracking-wider">Esperando al Comandante...</p>
<p className="text-xs text-gray-500 mt-2">La misión comenzará cuando el líder la orden.</p>
</div>
)}
</div>
</div>
</main>
);
}
return (
<GameBoard
gameState={gameState}
currentPlayerId={socket.id}
actions={actions}
/>
);
}
return (
<main className="relative min-h-screen flex flex-col items-center overflow-hidden bg-zinc-900 font-mono text-gray-200">
{/* FONDO COMÚN LOBBY/LOGIN */}
<div className="absolute inset-0 z-0 opacity-40">
<Image
src="/assets/images/ui/bg_lobby.png"
alt="Lobby Background"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent" />
</div>
{/* HEADER / LOGO */}
<div className="z-10 w-full p-4 flex justify-between items-center max-w-6xl">
<div className="flex items-center gap-4">
<Image src="/assets/images/ui/logo.png" alt="Logo" width={150} height={50} className="object-contain filter drop-shadow hidden md:block" />
<h1 className="text-2xl font-bold tracking-widest uppercase text-yellow-600">
Francia Ocupada
</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>
)}
</div>
<div className="z-10 w-full flex-1 flex flex-col items-center justify-center p-4">
<AnimatePresence mode="wait">
{/* --- PANTALLA DE LOGIN --- */}
{view === 'login' && (
<motion.form
key="login-form"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
onSubmit={handleLogin}
className="bg-black/80 p-8 rounded border border-white/20 shadow-2xl max-w-md w-full backdrop-blur-md"
>
<h2 className="text-xl text-center mb-6 uppercase tracking-[0.2em] text-white">Identificación</h2>
<div className="space-y-4">
<div>
<label className="text-xs uppercase text-gray-500 block mb-1">Nombre en Clave</label>
<input
required
value={playerName}
onChange={e => setPlayerName(e.target.value)}
className="w-full bg-white/10 border border-white/20 p-3 rounded text-white focus:outline-none focus:border-yellow-500 transition-colors"
placeholder="Ej: Agente"
/>
</div>
{/* APELLIDO ELIMINADO - SE GENERA AUTOMÁTICAMENTE */}
<button
type="submit"
className="w-full bg-yellow-700 hover:bg-yellow-600 text-white font-bold py-3 mt-4 rounded uppercase tracking-wider transition-all"
>
Acceder al Cuartel
</button>
</div>
</motion.form>
)}
{/* --- PANTALLA DE LOBBY (LISTA DE SALAS) --- */}
{view === 'lobby' && (
<motion.div
key="lobby-list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="w-full max-w-5xl"
>
<div className="flex justify-between items-end mb-6 border-b border-white/20 pb-4">
<div>
<h2 className="text-3xl font-light text-white">MISIONES ACTIVAS</h2>
<p className="text-gray-400 text-sm mt-1">Selecciona una operación o inicia una nueva.</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="bg-blue-800 hover:bg-blue-700 text-white px-6 py-2 rounded uppercase text-sm font-bold tracking-wider shadow-lg border border-blue-600 transition-all"
>
+ Crear Operación
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{roomsList.length === 0 ? (
<div className="col-span-full py-20 text-center text-gray-500 bg-black/30 rounded border border-white/5 border-dashed">
No hay misiones activas en este momento.
</div>
) : (
roomsList.map((room) => (
<motion.div
key={room.id}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="bg-black/60 border border-white/10 p-5 rounded hover:border-yellow-700/50 transition-colors group relative overflow-hidden"
>
<div className="absolute top-0 right-0 p-2">
{room.isPrivate ? (
<span title="Privada" className="text-red-400">🔒</span>
) : (
<span title="Pública" className="text-green-400/50">🔓</span>
)}
</div>
<h3 className="text-xl font-bold text-yellow-500 mb-1 group-hover:text-yellow-400 transition-colors">
{room.name}
</h3>
<div className="text-sm text-gray-400 mb-4 flex gap-2">
<span className="bg-white/10 px-2 py-0.5 rounded textxs">
HOST:
</span>
<span className="text-white">{room.hostId.substring(0, 6)}...</span>
</div>
<div className="flex justify-between items-center mt-4">
<div className="flex items-end gap-1">
<span className="text-3xl font-bold text-white">{room.currentPlayers}</span>
<span className="text-sm text-gray-500 mb-1">/ {room.maxPlayers}</span>
</div>
<button
disabled={room.currentPlayers >= room.maxPlayers || room.status !== 'waiting'}
onClick={() => requestJoinGame(room)}
className="bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded text-xs uppercase font-bold transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
{room.status === 'playing' ? 'EN CURSO' : (room.currentPlayers >= room.maxPlayers ? 'LLENA' : 'UNIRSE')}
</button>
</div>
{/* Barra de progreso visual */}
<div className="absolute bottom-0 left-0 h-1 bg-yellow-900/40 w-full">
<div
className="h-full bg-yellow-600 transition-all duration-500"
style={{ width: `${(room.currentPlayers / room.maxPlayers) * 100}%` }}
/>
</div>
</motion.div>
))
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* --- MODALES --- */}
{/* Modal Crear */}
{showCreateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<motion.div
initial={{ scale: 0.9 }} animate={{ scale: 1 }}
className="bg-zinc-800 p-6 rounded border border-white/20 w-full max-w-sm shadow-2xl"
>
<h3 className="text-lg font-bold text-white mb-4 uppercase">Configurar Operación</h3>
<form onSubmit={handleCreateGame} className="space-y-4">
<div>
<label className="block text-xs uppercase text-gray-400 mb-1"> Jugadores</label>
<select
value={createConfig.maxPlayers}
onChange={e => setCreateConfig({ ...createConfig, maxPlayers: Number(e.target.value) })}
className="w-full bg-black/40 border border-white/10 p-2 rounded text-white"
>
{[5, 6, 7, 8, 9, 10].map(n => (
<option key={n} value={n}>{n} Jugadores</option>
))}
</select>
</div>
<div>
<label className="block text-xs uppercase text-gray-400 mb-1">Contraseña (Opcional)</label>
<input
type="password"
value={createConfig.password}
onChange={e => setCreateConfig({ ...createConfig, password: e.target.value })}
className="w-full bg-black/40 border border-white/10 p-2 rounded text-white font-mono"
placeholder="Dejar vacío para pública"
/>
</div>
<div className="flex gap-2 mt-6">
<button type="button" onClick={() => setShowCreateModal(false)} className="flex-1 py-2 text-gray-400 hover:text-white transition-colors">Cancelar</button>
<button type="submit" className="flex-1 py-2 bg-yellow-700 hover:bg-yellow-600 text-white rounded font-bold uppercase">Crear</button>
</div>
</form>
</motion.div>
</div>
)}
{/* Modal Password */}
{passwordPromptRoomId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<motion.div
initial={{ scale: 0.9 }} animate={{ scale: 1 }}
className="bg-zinc-800 p-6 rounded border border-red-900/50 w-full max-w-sm shadow-2xl"
>
<h3 className="text-lg font-bold text-red-400 mb-2 uppercase flex items-center gap-2">
🔒 Acceso Restringido
</h3>
<p className="text-xs text-gray-400 mb-4">Esta operación es clasificada. Introduce la clave de acceso.</p>
<input
type="password"
autoFocus
value={joinPassword}
onChange={e => setJoinPassword(e.target.value)}
onKeyDown={e => e.key === 'Enter' && submitJoinPassword()}
className="w-full bg-black/40 border border-red-900/30 p-2 rounded text-white font-mono mb-4 focus:border-red-500 outline-none"
placeholder="Clave de acceso..."
/>
<div className="flex gap-2">
<button onClick={() => setPasswordPromptRoomId(null)} className="flex-1 py-2 text-gray-400 hover:text-white">Cancelar</button>
<button onClick={submitJoinPassword} className="flex-1 py-2 bg-red-900 hover:bg-red-800 text-white rounded font-bold uppercase">Acceder</button>
</div>
</motion.div>
</div>
)}
<div className="absolute bottom-2 right-4 text-[10px] text-gray-600 font-mono">
{isConnected ? <span className="text-green-900"> CONEXIÓN SEGURA</span> : <span className="text-red-900"> BUSCANDO SEÑAL...</span>}
</div>
</main>
);
}