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
This commit is contained in:
@@ -5,7 +5,7 @@ import './globals.css'
|
|||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'La Resistencia: WWII',
|
title: 'Francia Ocupada: WWII',
|
||||||
description: 'Juego de deducción social ambientado en la Segunda Guerra Mundial',
|
description: 'Juego de deducción social ambientado en la Segunda Guerra Mundial',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export default function Home() {
|
|||||||
<div className="flex items-center gap-4">
|
<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" />
|
<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">
|
<h1 className="text-2xl font-bold tracking-widest uppercase text-yellow-600">
|
||||||
La Resistencia
|
Francia Ocupada
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{view === 'lobby' && (
|
{view === 'lobby' && (
|
||||||
|
|||||||
@@ -63,26 +63,6 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
actions.voteMission(vote);
|
actions.voteMission(vote);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Componente de Debug para mostrar facción
|
|
||||||
const DebugInfo = () => (
|
|
||||||
<div className="fixed top-2 left-2 bg-black/80 text-white p-3 rounded-lg text-xs z-50 border border-yellow-500 font-mono shadow-xl pointer-events-none">
|
|
||||||
<div className="font-bold text-yellow-400 mb-1 flex items-center gap-2">
|
|
||||||
<span>🐛 DEBUG INFO</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div>Fase: <span className="text-cyan-400 font-bold">{gameState.phase}</span></div>
|
|
||||||
<div>ID: <span className="text-green-400">{currentPlayerId}</span></div>
|
|
||||||
<div>Facción: <span className={`font-bold ${currentPlayer?.faction === 'spies' ? 'text-red-500' : 'text-blue-400'}`}>
|
|
||||||
{currentPlayer?.faction || 'UNDEFINED'}
|
|
||||||
</span></div>
|
|
||||||
<div>Rol: <span className="text-purple-400">{currentPlayer?.role || 'UNDEFINED'}</span></div>
|
|
||||||
{gameState.currentLeaderId && (
|
|
||||||
<div>Líder: <span className="text-yellow-400">{gameState.currentLeaderId === currentPlayerId ? 'TÚ' : gameState.currentLeaderId}</span></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// Coordenadas porcentuales de los hexágonos de misión en el mapa
|
// Coordenadas porcentuales de los hexágonos de misión en el mapa
|
||||||
const missionCoords = [
|
const missionCoords = [
|
||||||
@@ -109,12 +89,14 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
Guerra Total
|
Guerra Total
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Audio Auto-Play */}
|
{/* Audio Auto-Play - Solo para el host */}
|
||||||
<audio
|
{isHost && (
|
||||||
src="/assets/audio/Intro.ogg"
|
<audio
|
||||||
autoPlay
|
src="/assets/audio/Intro.ogg"
|
||||||
onEnded={() => isHost && actions.finishIntro()}
|
autoPlay
|
||||||
/>
|
onEnded={() => actions.finishIntro()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isHost && (
|
{isHost && (
|
||||||
<button
|
<button
|
||||||
@@ -132,31 +114,31 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
|
|
||||||
if (gameState.phase === 'reveal_role' as any) {
|
if (gameState.phase === 'reveal_role' as any) {
|
||||||
// Determinar imagen basada en el rol
|
// Determinar imagen basada en el rol
|
||||||
// Mapeo básico:
|
// Mapeo actualizado:
|
||||||
// Merlin -> good_merlin.png
|
// Marlene -> good_merlin.png
|
||||||
// Percival -> good_percival.png
|
// Capitán Philippe -> good_percival.png
|
||||||
// Servant -> good_soldier_X.png (random)
|
// Partisano -> good_soldier_X.png (random)
|
||||||
// Assassin -> evil_assassin.png
|
// Francotirador -> evil_assassin.png
|
||||||
// Morgana -> evil_morgana.png
|
// Agente Doble -> evil_morgana.png
|
||||||
// Mordred -> evil_mordred.png
|
// Comandante Schmidt -> evil_mordred.png
|
||||||
// Oberon -> evil_oberon.png
|
// Infiltrado -> evil_oberon.png
|
||||||
// Minion -> evil_minion_X.png
|
// Colaboracionista -> evil_minion_X.png
|
||||||
|
|
||||||
let roleImage = '/assets/images/characters/good_soldier_1.png'; // Default
|
let roleImage = '/assets/images/characters/good_soldier_1.png'; // Default
|
||||||
|
|
||||||
const role = currentPlayer?.role;
|
const role = currentPlayer?.role;
|
||||||
if (role === 'merlin') roleImage = '/assets/images/characters/good_merlin.png';
|
if (role === 'marlene') roleImage = '/assets/images/characters/good_merlin.png';
|
||||||
else if (role === 'assassin') roleImage = '/assets/images/characters/evil_assassin.png';
|
else if (role === 'francotirador') roleImage = '/assets/images/characters/evil_assassin.png';
|
||||||
else if (role === 'percival') roleImage = '/assets/images/characters/good_percival.png';
|
else if (role === 'capitan_philippe') roleImage = '/assets/images/characters/good_percival.png';
|
||||||
else if (role === 'morgana') roleImage = '/assets/images/characters/evil_morgana.png';
|
else if (role === 'agente_doble') roleImage = '/assets/images/characters/evil_morgana.png';
|
||||||
else if (role === 'mordred') roleImage = '/assets/images/characters/evil_mordred.png';
|
else if (role === 'comandante_schmidt') roleImage = '/assets/images/characters/evil_mordred.png';
|
||||||
else if (role === 'oberon') roleImage = '/assets/images/characters/evil_oberon.png';
|
else if (role === 'infiltrado') roleImage = '/assets/images/characters/evil_oberon.png';
|
||||||
else if (role === 'loyal_servant') {
|
else if (role === 'partisano') {
|
||||||
// Random soldier 1-5
|
// Random soldier 1-5
|
||||||
const idx = (currentPlayerId.charCodeAt(0) % 5) + 1;
|
const idx = (currentPlayerId.charCodeAt(0) % 5) + 1;
|
||||||
roleImage = `/assets/images/characters/good_soldier_${idx}.png`;
|
roleImage = `/assets/images/characters/good_soldier_${idx}.png`;
|
||||||
}
|
}
|
||||||
else if (role === 'minion') {
|
else if (role === 'colaboracionista') {
|
||||||
// Random minion 1-3
|
// Random minion 1-3
|
||||||
const idx = (currentPlayerId.charCodeAt(0) % 3) + 1;
|
const idx = (currentPlayerId.charCodeAt(0) % 3) + 1;
|
||||||
roleImage = `/assets/images/characters/evil_minion_${idx}.png`;
|
roleImage = `/assets/images/characters/evil_minion_${idx}.png`;
|
||||||
@@ -228,8 +210,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
if (gameState.phase === 'roll_call' as any) {
|
if (gameState.phase === 'roll_call' as any) {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-screen flex flex-col items-center justify-center bg-black overflow-hidden text-white font-mono">
|
<div className="relative w-full h-screen flex flex-col items-center justify-center bg-black overflow-hidden text-white font-mono">
|
||||||
{/* Debug Info */}
|
|
||||||
<DebugInfo />
|
|
||||||
|
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image src="/assets/images/ui/bg_roll_call.png" alt="Resistance HQ" fill className="object-cover" />
|
<Image src="/assets/images/ui/bg_roll_call.png" alt="Resistance HQ" fill className="object-cover" />
|
||||||
@@ -251,8 +232,6 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
|
||||||
{gameState.players.map((p, i) => {
|
{gameState.players.map((p, i) => {
|
||||||
// Asignar avatar determinista basado en charCode
|
|
||||||
const avatarIdx = (p.name.length % 3) + 1;
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
@@ -263,7 +242,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
>
|
>
|
||||||
<div className="w-32 h-32 rounded-full border-4 border-gray-400 overflow-hidden relative shadow-2xl bg-black">
|
<div className="w-32 h-32 rounded-full border-4 border-gray-400 overflow-hidden relative shadow-2xl bg-black">
|
||||||
<Image
|
<Image
|
||||||
src={`/assets/images/characters/avatar_${avatarIdx}.png`}
|
src={`/assets/images/characters/${p.avatar}`}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
fill
|
fill
|
||||||
className="object-cover grayscale contrast-125"
|
className="object-cover grayscale contrast-125"
|
||||||
@@ -283,8 +262,6 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-screen flex flex-col items-center overflow-hidden">
|
<div className="relative w-full h-screen flex flex-col items-center overflow-hidden">
|
||||||
{/* Debug Info */}
|
|
||||||
<DebugInfo />
|
|
||||||
|
|
||||||
<div className="absolute inset-0 z-0 opacity-40">
|
<div className="absolute inset-0 z-0 opacity-40">
|
||||||
<Image src="/assets/images/ui/bg_game.png" alt="Game Background" fill className="object-cover" />
|
<Image src="/assets/images/ui/bg_game.png" alt="Game Background" fill className="object-cover" />
|
||||||
@@ -310,8 +287,12 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="absolute w-[12%] aspect-square flex items-center justify-center transform -translate-x-1/2 -translate-y-1/2"
|
className="absolute w-[10%] aspect-square flex items-center justify-center"
|
||||||
style={{ left: coord.left, top: coord.top }}
|
style={{
|
||||||
|
left: coord.left,
|
||||||
|
top: coord.top,
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Marcador de Ronda Actual */}
|
{/* Marcador de Ronda Actual */}
|
||||||
{isCurrent && (
|
{isCurrent && (
|
||||||
@@ -335,17 +316,21 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
{result === true && (
|
{result === true && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }} animate={{ scale: 1 }}
|
initial={{ scale: 0 }} animate={{ scale: 1 }}
|
||||||
className="absolute inset-0 z-20"
|
className="absolute inset-0 z-20 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Image src="/assets/images/tokens/marker_score_blue.png" alt="Success" fill className="object-contain drop-shadow-lg" />
|
<div className="w-[80%] h-[80%] relative">
|
||||||
|
<Image src="/assets/images/tokens/marker_score_blue.png" alt="Success" fill className="object-contain drop-shadow-lg" />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{result === false && (
|
{result === false && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }} animate={{ scale: 1 }}
|
initial={{ scale: 0 }} animate={{ scale: 1 }}
|
||||||
className="absolute inset-0 z-20"
|
className="absolute inset-0 z-20 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Image src="/assets/images/tokens/marker_score_red.png" alt="Fail" fill className="object-contain drop-shadow-lg" />
|
<div className="w-[80%] h-[80%] relative">
|
||||||
|
<Image src="/assets/images/tokens/marker_score_red.png" alt="Fail" fill className="object-contain drop-shadow-lg" />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -633,6 +618,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
|||||||
{gameState.phase === 'mission_result' as any && (
|
{gameState.phase === 'mission_result' as any && (
|
||||||
<MissionResult
|
<MissionResult
|
||||||
gameState={gameState}
|
gameState={gameState}
|
||||||
|
isHost={isHost}
|
||||||
onContinue={() => isHost && actions.finishMissionResult()}
|
onContinue={() => isHost && actions.finishMissionResult()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { GameState } from '../../../shared/types';
|
|||||||
interface MissionResultProps {
|
interface MissionResultProps {
|
||||||
gameState: GameState;
|
gameState: GameState;
|
||||||
onContinue: () => void;
|
onContinue: () => void;
|
||||||
|
isHost: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MissionResult({ gameState, onContinue }: MissionResultProps) {
|
export default function MissionResult({ gameState, onContinue, isHost }: MissionResultProps) {
|
||||||
// Obtener la última misión del historial
|
// Obtener la última misión del historial
|
||||||
const lastMission = gameState.missionHistory[gameState.missionHistory.length - 1];
|
const lastMission = gameState.missionHistory[gameState.missionHistory.length - 1];
|
||||||
|
|
||||||
if (!lastMission) {
|
if (!lastMission) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-black/90 z-50">
|
<div className="fixed inset-0 flex items-center justify-center bg-black/90 z-50">
|
||||||
@@ -27,15 +28,15 @@ export default function MissionResult({ gameState, onContinue }: MissionResultPr
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
>
|
>
|
||||||
<motion.h2
|
<motion.h2
|
||||||
className={`text - 6xl md: text - 7xl font - bold mb - 8 ${ isSuccess ? 'text-blue-500' : 'text-red-500' } `}
|
className={`text-6xl md:text-7xl font-bold mb-8 ${isSuccess ? 'text-blue-500' : 'text-red-500'}`}
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
|
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
|
||||||
>
|
>
|
||||||
{isSuccess ? '¡MISIÓN EXITOSA!' : 'MISIÓN FALLIDA'}
|
{isSuccess ? '¡MISIÓN EXITOSA!' : 'MISIÓN FALLIDA'}
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-white text-3xl mb-8 bg-black/50 p-6 rounded-xl"
|
className="text-white text-3xl mb-8 bg-black/50 p-6 rounded-xl"
|
||||||
initial={{ y: 50, opacity: 0 }}
|
initial={{ y: 50, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
@@ -53,22 +54,33 @@ export default function MissionResult({ gameState, onContinue }: MissionResultPr
|
|||||||
>
|
>
|
||||||
<p>Misión {gameState.currentRound} de 5</p>
|
<p>Misión {gameState.currentRound} de 5</p>
|
||||||
<p className="text-gray-400 text-sm mt-2">
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
Resistencia: {gameState.missionHistory.filter(m => m.isSuccess).length} |
|
Resistencia: {gameState.missionHistory.filter(m => m.isSuccess).length} |
|
||||||
Espías: {gameState.missionHistory.filter(m => !m.isSuccess).length}
|
Espías: {gameState.missionHistory.filter(m => !m.isSuccess).length}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.button
|
{isHost ? (
|
||||||
onClick={onContinue}
|
<motion.button
|
||||||
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-bold py-4 px-8 rounded-lg text-xl shadow-lg transform transition-all hover:scale-105"
|
onClick={onContinue}
|
||||||
initial={{ y: 50, opacity: 0 }}
|
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-bold py-4 px-8 rounded-lg text-xl shadow-lg transform transition-all hover:scale-105"
|
||||||
animate={{ y: 0, opacity: 1 }}
|
initial={{ y: 50, opacity: 0 }}
|
||||||
transition={{ delay: 1.5 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
whileHover={{ scale: 1.05 }}
|
transition={{ delay: 1.5 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
>
|
whileTap={{ scale: 0.95 }}
|
||||||
CONTINUAR →
|
>
|
||||||
</motion.button>
|
CONTINUAR →
|
||||||
|
</motion.button>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
className="text-white text-lg font-mono bg-black/50 px-6 py-3 rounded-full border border-white/20 animate-pulse"
|
||||||
|
initial={{ y: 50, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 1.5 }}
|
||||||
|
>
|
||||||
|
Esperando al comandante...
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
interface MissionRevealProps {
|
interface MissionRevealProps {
|
||||||
votes: boolean[];
|
votes: boolean[];
|
||||||
@@ -7,11 +8,11 @@ interface MissionRevealProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MissionReveal({ votes, onFinished }: MissionRevealProps) {
|
export default function MissionReveal({ votes, onFinished }: MissionRevealProps) {
|
||||||
// Timer de seguridad: 10 segundos y avanza
|
// Timer de seguridad: 5 segundos y avanza
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (onFinished) onFinished();
|
if (onFinished) onFinished();
|
||||||
}, 10000);
|
}, 5000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [onFinished]);
|
}, [onFinished]);
|
||||||
@@ -30,20 +31,22 @@ export default function MissionReveal({ votes, onFinished }: MissionRevealProps)
|
|||||||
{votes.map((vote, idx) => (
|
{votes.map((vote, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`w-48 h-72 rounded-xl flex items-center justify-center text-7xl font-bold text-white shadow-2xl border-4 ${vote
|
className="w-48 h-72 rounded-xl flex items-center justify-center shadow-2xl relative overflow-hidden"
|
||||||
? 'bg-gradient-to-br from-blue-600 to-blue-900 border-blue-400 shadow-blue-500/50'
|
|
||||||
: 'bg-gradient-to-br from-red-600 to-red-900 border-red-400 shadow-red-500/50'
|
|
||||||
}`}
|
|
||||||
initial={{ scale: 0, rotateY: 180 }}
|
initial={{ scale: 0, rotateY: 180 }}
|
||||||
animate={{ scale: 1, rotateY: 0 }}
|
animate={{ scale: 1, rotateY: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
delay: idx * 0.8, // Más lento para drama
|
delay: idx * 0.3,
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 200,
|
stiffness: 200,
|
||||||
damping: 20
|
damping: 20
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{vote ? '✓' : '✗'}
|
<Image
|
||||||
|
src={vote ? '/assets/images/tokens/vote_approve.png' : '/assets/images/tokens/vote_reject.png'}
|
||||||
|
alt={vote ? 'Éxito' : 'Sabotaje'}
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +55,7 @@ export default function MissionReveal({ votes, onFinished }: MissionRevealProps)
|
|||||||
className="text-white text-xl font-mono mt-8"
|
className="text-white text-xl font-mono mt-8"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: votes.length * 0.8 + 1 }}
|
transition={{ delay: votes.length * 0.3 + 0.5 }}
|
||||||
>
|
>
|
||||||
<span className="animate-pulse">Analizando resultado estratégico...</span>
|
<span className="animate-pulse">Analizando resultado estratégico...</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const io = new Server(server, {
|
|||||||
// En el futuro esto podría estar en Redis o Postgres
|
// En el futuro esto podría estar en Redis o Postgres
|
||||||
const games: Record<string, Game> = {};
|
const games: Record<string, Game> = {};
|
||||||
|
|
||||||
|
// Almacén de timers para auto-resolución de votaciones
|
||||||
|
const voteTimers: Record<string, NodeJS.Timeout> = {};
|
||||||
|
|
||||||
// --- LOBBY MANAGEMENT ---
|
// --- LOBBY MANAGEMENT ---
|
||||||
|
|
||||||
const MISSION_NAMES = [
|
const MISSION_NAMES = [
|
||||||
@@ -38,6 +41,24 @@ const MISSION_NAMES = [
|
|||||||
"Operación Eiche", "Operación León Marino", "Operación Urano"
|
"Operación Eiche", "Operación León Marino", "Operación Urano"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Helper para iniciar timer de votación de líder
|
||||||
|
function startLeaderVoteTimer(roomId: string) {
|
||||||
|
if (voteTimers[roomId]) clearTimeout(voteTimers[roomId]);
|
||||||
|
voteTimers[roomId] = setTimeout(() => {
|
||||||
|
const g = games[roomId];
|
||||||
|
if (g && g.state.phase === 'vote_leader') {
|
||||||
|
// Pasar al siguiente líder sin resolver (rechazar implícitamente)
|
||||||
|
g.nextLeader();
|
||||||
|
io.to(roomId).emit('game_state', g.state);
|
||||||
|
|
||||||
|
// Si sigue en vote_leader (nuevo líder), reiniciar timer
|
||||||
|
if (g.state.phase === 'vote_leader') {
|
||||||
|
startLeaderVoteTimer(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 11000); // 11 segundos
|
||||||
|
}
|
||||||
|
|
||||||
const generateRoomName = () => {
|
const generateRoomName = () => {
|
||||||
const idx = Math.floor(Math.random() * MISSION_NAMES.length);
|
const idx = Math.floor(Math.random() * MISSION_NAMES.length);
|
||||||
const suffix = Math.floor(100 + Math.random() * 900); // 3 digit code
|
const suffix = Math.floor(100 + Math.random() * 900); // 3 digit code
|
||||||
@@ -156,6 +177,10 @@ io.on('connection', (socket) => {
|
|||||||
// ERROR CORREGIDO: No llamar a startGame() de nuevo porque re-baraja los roles.
|
// 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.
|
// Simplemente avanzamos a la fase de votación de líder que ya estaba configurada.
|
||||||
game.state.phase = 'vote_leader' as any;
|
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);
|
io.to(roomId).emit('game_state', game.state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -165,8 +190,23 @@ io.on('connection', (socket) => {
|
|||||||
socket.on('vote_leader', ({ roomId, approve }) => {
|
socket.on('vote_leader', ({ roomId, approve }) => {
|
||||||
const game = games[roomId];
|
const game = games[roomId];
|
||||||
if (game) {
|
if (game) {
|
||||||
|
const previousPhase = game.state.phase;
|
||||||
game.voteLeader(socket.id, approve);
|
game.voteLeader(socket.id, approve);
|
||||||
io.to(roomId).emit('game_state', game.state);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,21 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addPlayer(id: string, name: string): Player {
|
addPlayer(id: string, name: string): Player {
|
||||||
// Asignar avatar aleatorio persistente (rebel001.jpg - rebel010.jpg)
|
// Asignar avatar aleatorio sin repetir (rebel001.jpg - rebel010.jpg)
|
||||||
const avatarIdx = Math.floor(Math.random() * 10) + 1;
|
// Obtener avatares ya usados
|
||||||
const avatarStr = `rebel${avatarIdx.toString().padStart(3, '0')}.jpg`;
|
const usedAvatars = this.state.players.map(p => p.avatar);
|
||||||
|
|
||||||
|
// Crear lista de avatares disponibles
|
||||||
|
const allAvatars = Array.from({ length: 10 }, (_, i) =>
|
||||||
|
`rebel${(i + 1).toString().padStart(3, '0')}.jpg`
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableAvatars = allAvatars.filter(av => !usedAvatars.includes(av));
|
||||||
|
|
||||||
|
// Si no hay avatares disponibles (más de 10 jugadores), usar cualquiera
|
||||||
|
const avatarStr = availableAvatars.length > 0
|
||||||
|
? availableAvatars[Math.floor(Math.random() * availableAvatars.length)]
|
||||||
|
: allAvatars[Math.floor(Math.random() * allAvatars.length)];
|
||||||
|
|
||||||
const player: Player = {
|
const player: Player = {
|
||||||
id,
|
id,
|
||||||
@@ -92,13 +104,13 @@ export class Game {
|
|||||||
// ... assignRoles se mantiene igual ...
|
// ... assignRoles se mantiene igual ...
|
||||||
private assignRoles(goodCount: number, evilCount: number) {
|
private assignRoles(goodCount: number, evilCount: number) {
|
||||||
// Roles obligatorios
|
// Roles obligatorios
|
||||||
const roles: Role[] = [Role.MERLIN, Role.ASSASSIN];
|
const roles: Role[] = [Role.MARLENE, Role.FRANCOTIRADOR]; // Updated roles
|
||||||
|
|
||||||
// Rellenar resto de malos
|
// Rellenar resto de malos
|
||||||
for (let i = 0; i < evilCount - 1; i++) roles.push(Role.MINION);
|
for (let i = 0; i < evilCount - 1; i++) roles.push(Role.COLABORACIONISTA); // Updated role
|
||||||
|
|
||||||
// Rellenar resto de buenos
|
// Rellenar resto de buenos
|
||||||
for (let i = 0; i < goodCount - 1; i++) roles.push(Role.LOYAL_SERVANT);
|
for (let i = 0; i < goodCount - 1; i++) roles.push(Role.PARTISANO); // Updated role
|
||||||
|
|
||||||
// Barajar roles
|
// Barajar roles
|
||||||
const shuffledRoles = roles.sort(() => Math.random() - 0.5);
|
const shuffledRoles = roles.sort(() => Math.random() - 0.5);
|
||||||
@@ -107,10 +119,10 @@ export class Game {
|
|||||||
this.state.players.forEach((player, index) => {
|
this.state.players.forEach((player, index) => {
|
||||||
player.role = shuffledRoles[index];
|
player.role = shuffledRoles[index];
|
||||||
// Asignar facción basada en el rol
|
// Asignar facción basada en el rol
|
||||||
if ([Role.MERLIN, Role.PERCIVAL, Role.LOYAL_SERVANT].includes(player.role)) {
|
if ([Role.MARLENE, Role.PARTISANO].includes(player.role)) { // Updated roles
|
||||||
player.faction = Faction.RESISTANCE;
|
player.faction = Faction.ALIADOS;
|
||||||
} else {
|
} else {
|
||||||
player.faction = Faction.SPIES;
|
player.faction = Faction.ALEMANES;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,12 +137,21 @@ export class Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Método para forzar la resolución de votos (llamado por timeout)
|
||||||
|
forceResolveLeaderVote() {
|
||||||
|
// Registrar votos null para los que no votaron
|
||||||
|
this.state.players.forEach(player => {
|
||||||
|
if (!(player.id in this.state.leaderVotes)) {
|
||||||
|
this.state.leaderVotes[player.id] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.resolveLeaderVote();
|
||||||
|
}
|
||||||
|
|
||||||
private resolveLeaderVote() {
|
private resolveLeaderVote() {
|
||||||
const votes = Object.values(this.state.leaderVotes);
|
const votes = Object.values(this.state.leaderVotes);
|
||||||
const approves = votes.filter(v => v === true).length;
|
const approves = votes.filter(v => v === true).length;
|
||||||
const rejects = votes.filter(v => v === false).length;
|
const rejects = votes.filter(v => v === false || v === null).length; // null cuenta como rechazo
|
||||||
// Los nulos (timeout) no suman a ninguno, o cuentan como reject implícito?
|
|
||||||
// "Si llega a 0... su voto no cuenta". Simplemente no suma.
|
|
||||||
|
|
||||||
this.log(`Votación de Líder: ${approves} A favor - ${rejects} En contra.`);
|
this.log(`Votación de Líder: ${approves} A favor - ${rejects} En contra.`);
|
||||||
|
|
||||||
@@ -189,7 +210,7 @@ export class Game {
|
|||||||
this.state.proposedTeam = [];
|
this.state.proposedTeam = [];
|
||||||
|
|
||||||
if (this.state.failedVotesCount >= 5) {
|
if (this.state.failedVotesCount >= 5) {
|
||||||
this.endGame(Faction.SPIES, 'Se han rechazado 5 equipos consecutivos.');
|
this.endGame(Faction.ALEMANES, 'Se han rechazado 5 equipos consecutivos.'); // Updated Faction
|
||||||
} else {
|
} else {
|
||||||
this.nextLeader(); // Pasa a VOTE_LEADER
|
this.nextLeader(); // Pasa a VOTE_LEADER
|
||||||
this.log('El equipo fue rechazado. El liderazgo pasa al siguiente jugador.');
|
this.log('El equipo fue rechazado. El liderazgo pasa al siguiente jugador.');
|
||||||
@@ -244,10 +265,7 @@ export class Game {
|
|||||||
|
|
||||||
this.log(`Misión ${round} completada. Revelando votos...`);
|
this.log(`Misión ${round} completada. Revelando votos...`);
|
||||||
|
|
||||||
// Auto-avanzar a MISSION_RESULT después de 5 segundos
|
// El cliente controlará el avance a MISSION_RESULT con su timer
|
||||||
setTimeout(() => {
|
|
||||||
this.finishReveal();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -268,7 +286,7 @@ export class Game {
|
|||||||
const failures = this.state.questResults.filter(r => r === false).length;
|
const failures = this.state.questResults.filter(r => r === false).length;
|
||||||
|
|
||||||
if (failures >= 3) {
|
if (failures >= 3) {
|
||||||
this.endGame(Faction.SPIES, 'Tres misiones han fracasado.');
|
this.endGame(Faction.ALEMANES, 'Tres misiones han fracasado.'); // Updated Faction
|
||||||
} else if (successes >= 3) {
|
} else if (successes >= 3) {
|
||||||
this.state.phase = GamePhase.ASSASSIN_PHASE;
|
this.state.phase = GamePhase.ASSASSIN_PHASE;
|
||||||
this.log('¡La Resistencia ha triunfado! Pero el Asesino tiene una última oportunidad...');
|
this.log('¡La Resistencia ha triunfado! Pero el Asesino tiene una última oportunidad...');
|
||||||
@@ -284,14 +302,14 @@ export class Game {
|
|||||||
|
|
||||||
assassinKill(targetId: string) {
|
assassinKill(targetId: string) {
|
||||||
const target = this.state.players.find(p => p.id === targetId);
|
const target = this.state.players.find(p => p.id === targetId);
|
||||||
if (target && target.role === Role.MERLIN) {
|
if (target && target.role === Role.MARLENE) { // Updated Role
|
||||||
this.endGame(Faction.SPIES, '¡El Asesino ha eliminado a Marlenne (Merlín)!');
|
this.endGame(Faction.ALEMANES, '¡El Asesino ha eliminado a Marlene!'); // Updated Faction and message
|
||||||
} else {
|
} else {
|
||||||
this.endGame(Faction.RESISTANCE, 'El Asesino ha fallado. ¡La Resistencia gana!');
|
this.endGame(Faction.ALIADOS, 'El Asesino ha fallado. ¡La Resistencia gana!'); // Updated Faction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private nextLeader() {
|
nextLeader() {
|
||||||
const currentIdx = this.state.players.findIndex(p => p.id === this.state.currentLeaderId);
|
const currentIdx = this.state.players.findIndex(p => p.id === this.state.currentLeaderId);
|
||||||
const nextIdx = (currentIdx + 1) % this.state.players.length;
|
const nextIdx = (currentIdx + 1) % this.state.players.length;
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
export enum Role {
|
export enum Role {
|
||||||
// Bando del Bien (Resistencia Francesa)
|
// Bando Aliado (Resistencia Francesa)
|
||||||
MERLIN = 'merlin', // Marlenne
|
MARLENE = 'marlene', // Agente de inteligencia (antes Merlin)
|
||||||
PERCIVAL = 'percival',
|
CAPITAN_PHILIPPE = 'capitan_philippe', // Oficial que conoce a Marlene (antes Percival)
|
||||||
LOYAL_SERVANT = 'loyal_servant', // Soldado Resistencia
|
PARTISANO = 'partisano', // Miembro leal de la resistencia (antes Loyal Servant)
|
||||||
|
|
||||||
// Bando del Mal (Ocupación Alemana)
|
// Bando Alemán (Ocupación Nazi)
|
||||||
MORDRED = 'mordred',
|
COMANDANTE_SCHMIDT = 'comandante_schmidt', // Oficial nazi oculto (antes Mordred)
|
||||||
ASSASSIN = 'assassin',
|
FRANCOTIRADOR = 'francotirador', // Puede eliminar a Marlene (antes Assassin)
|
||||||
MORGANA = 'morgana',
|
AGENTE_DOBLE = 'agente_doble', // Se hace pasar por Marlene (antes Morgana)
|
||||||
OBERON = 'oberon',
|
INFILTRADO = 'infiltrado', // Espía solitario (antes Oberon)
|
||||||
MINION = 'minion', // Soldado Alemán
|
COLABORACIONISTA = 'colaboracionista', // Espía genérico (antes Minion/Spy)
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Faction {
|
export enum Faction {
|
||||||
RESISTANCE = 'resistance',
|
ALIADOS = 'aliados', // Antes RESISTANCE
|
||||||
SPIES = 'spies',
|
ALEMANES = 'alemanes', // Antes SPIES
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GamePhase {
|
export enum GamePhase {
|
||||||
|
|||||||
Reference in New Issue
Block a user