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'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'La Resistencia: WWII',
|
||||
title: 'Francia Ocupada: WWII',
|
||||
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">
|
||||
<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">
|
||||
La Resistencia
|
||||
Francia Ocupada
|
||||
</h1>
|
||||
</div>
|
||||
{view === 'lobby' && (
|
||||
|
||||
@@ -63,26 +63,6 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
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
|
||||
const missionCoords = [
|
||||
@@ -109,12 +89,14 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
Guerra Total
|
||||
</h1>
|
||||
|
||||
{/* Audio Auto-Play */}
|
||||
<audio
|
||||
src="/assets/audio/Intro.ogg"
|
||||
autoPlay
|
||||
onEnded={() => isHost && actions.finishIntro()}
|
||||
/>
|
||||
{/* Audio Auto-Play - Solo para el host */}
|
||||
{isHost && (
|
||||
<audio
|
||||
src="/assets/audio/Intro.ogg"
|
||||
autoPlay
|
||||
onEnded={() => actions.finishIntro()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHost && (
|
||||
<button
|
||||
@@ -132,31 +114,31 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
if (gameState.phase === 'reveal_role' as any) {
|
||||
// Determinar imagen basada en el rol
|
||||
// Mapeo básico:
|
||||
// Merlin -> good_merlin.png
|
||||
// Percival -> good_percival.png
|
||||
// Servant -> good_soldier_X.png (random)
|
||||
// Assassin -> evil_assassin.png
|
||||
// Morgana -> evil_morgana.png
|
||||
// Mordred -> evil_mordred.png
|
||||
// Oberon -> evil_oberon.png
|
||||
// Minion -> evil_minion_X.png
|
||||
// Mapeo actualizado:
|
||||
// Marlene -> good_merlin.png
|
||||
// Capitán Philippe -> good_percival.png
|
||||
// Partisano -> good_soldier_X.png (random)
|
||||
// Francotirador -> evil_assassin.png
|
||||
// Agente Doble -> evil_morgana.png
|
||||
// Comandante Schmidt -> evil_mordred.png
|
||||
// Infiltrado -> evil_oberon.png
|
||||
// Colaboracionista -> evil_minion_X.png
|
||||
|
||||
let roleImage = '/assets/images/characters/good_soldier_1.png'; // Default
|
||||
|
||||
const role = currentPlayer?.role;
|
||||
if (role === 'merlin') roleImage = '/assets/images/characters/good_merlin.png';
|
||||
else if (role === 'assassin') roleImage = '/assets/images/characters/evil_assassin.png';
|
||||
else if (role === 'percival') roleImage = '/assets/images/characters/good_percival.png';
|
||||
else if (role === 'morgana') roleImage = '/assets/images/characters/evil_morgana.png';
|
||||
else if (role === 'mordred') roleImage = '/assets/images/characters/evil_mordred.png';
|
||||
else if (role === 'oberon') roleImage = '/assets/images/characters/evil_oberon.png';
|
||||
else if (role === 'loyal_servant') {
|
||||
if (role === 'marlene') roleImage = '/assets/images/characters/good_merlin.png';
|
||||
else if (role === 'francotirador') roleImage = '/assets/images/characters/evil_assassin.png';
|
||||
else if (role === 'capitan_philippe') roleImage = '/assets/images/characters/good_percival.png';
|
||||
else if (role === 'agente_doble') roleImage = '/assets/images/characters/evil_morgana.png';
|
||||
else if (role === 'comandante_schmidt') roleImage = '/assets/images/characters/evil_mordred.png';
|
||||
else if (role === 'infiltrado') roleImage = '/assets/images/characters/evil_oberon.png';
|
||||
else if (role === 'partisano') {
|
||||
// Random soldier 1-5
|
||||
const idx = (currentPlayerId.charCodeAt(0) % 5) + 1;
|
||||
roleImage = `/assets/images/characters/good_soldier_${idx}.png`;
|
||||
}
|
||||
else if (role === 'minion') {
|
||||
else if (role === 'colaboracionista') {
|
||||
// Random minion 1-3
|
||||
const idx = (currentPlayerId.charCodeAt(0) % 3) + 1;
|
||||
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) {
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
{gameState.players.map((p, i) => {
|
||||
// Asignar avatar determinista basado en charCode
|
||||
const avatarIdx = (p.name.length % 3) + 1;
|
||||
return (
|
||||
<motion.div
|
||||
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">
|
||||
<Image
|
||||
src={`/assets/images/characters/avatar_${avatarIdx}.png`}
|
||||
src={`/assets/images/characters/${p.avatar}`}
|
||||
alt="Avatar"
|
||||
fill
|
||||
className="object-cover grayscale contrast-125"
|
||||
@@ -283,8 +262,6 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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 (
|
||||
<div
|
||||
key={idx}
|
||||
className="absolute w-[12%] aspect-square flex items-center justify-center transform -translate-x-1/2 -translate-y-1/2"
|
||||
style={{ left: coord.left, top: coord.top }}
|
||||
className="absolute w-[10%] aspect-square flex items-center justify-center"
|
||||
style={{
|
||||
left: coord.left,
|
||||
top: coord.top,
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
{/* Marcador de Ronda Actual */}
|
||||
{isCurrent && (
|
||||
@@ -335,17 +316,21 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
{result === true && (
|
||||
<motion.div
|
||||
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>
|
||||
)}
|
||||
{result === false && (
|
||||
<motion.div
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
@@ -633,6 +618,7 @@ export default function GameBoard({ gameState, currentPlayerId, actions }: GameB
|
||||
{gameState.phase === 'mission_result' as any && (
|
||||
<MissionResult
|
||||
gameState={gameState}
|
||||
isHost={isHost}
|
||||
onContinue={() => isHost && actions.finishMissionResult()}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { GameState } from '../../../shared/types';
|
||||
interface MissionResultProps {
|
||||
gameState: GameState;
|
||||
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
|
||||
const lastMission = gameState.missionHistory[gameState.missionHistory.length - 1];
|
||||
|
||||
@@ -27,7 +28,7 @@ export default function MissionResult({ gameState, onContinue }: MissionResultPr
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<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 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
|
||||
@@ -58,17 +59,28 @@ export default function MissionResult({ gameState, onContinue }: MissionResultPr
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.button
|
||||
onClick={onContinue}
|
||||
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"
|
||||
initial={{ y: 50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 1.5 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
CONTINUAR →
|
||||
</motion.button>
|
||||
{isHost ? (
|
||||
<motion.button
|
||||
onClick={onContinue}
|
||||
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"
|
||||
initial={{ y: 50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 1.5 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface MissionRevealProps {
|
||||
votes: boolean[];
|
||||
@@ -7,11 +8,11 @@ interface MissionRevealProps {
|
||||
}
|
||||
|
||||
export default function MissionReveal({ votes, onFinished }: MissionRevealProps) {
|
||||
// Timer de seguridad: 10 segundos y avanza
|
||||
// Timer de seguridad: 5 segundos y avanza
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (onFinished) onFinished();
|
||||
}, 10000);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [onFinished]);
|
||||
@@ -30,20 +31,22 @@ export default function MissionReveal({ votes, onFinished }: MissionRevealProps)
|
||||
{votes.map((vote, idx) => (
|
||||
<motion.div
|
||||
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
|
||||
? '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'
|
||||
}`}
|
||||
className="w-48 h-72 rounded-xl flex items-center justify-center shadow-2xl relative overflow-hidden"
|
||||
initial={{ scale: 0, rotateY: 180 }}
|
||||
animate={{ scale: 1, rotateY: 0 }}
|
||||
transition={{
|
||||
delay: idx * 0.8, // Más lento para drama
|
||||
delay: idx * 0.3,
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
@@ -52,7 +55,7 @@ export default function MissionReveal({ votes, onFinished }: MissionRevealProps)
|
||||
className="text-white text-xl font-mono mt-8"
|
||||
initial={{ opacity: 0 }}
|
||||
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>
|
||||
</motion.div>
|
||||
|
||||
@@ -28,6 +28,9 @@ const io = new Server(server, {
|
||||
// En el futuro esto podría estar en Redis o Postgres
|
||||
const games: Record<string, Game> = {};
|
||||
|
||||
// Almacén de timers para auto-resolución de votaciones
|
||||
const voteTimers: Record<string, NodeJS.Timeout> = {};
|
||||
|
||||
// --- LOBBY MANAGEMENT ---
|
||||
|
||||
const MISSION_NAMES = [
|
||||
@@ -38,6 +41,24 @@ const MISSION_NAMES = [
|
||||
"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 idx = Math.floor(Math.random() * MISSION_NAMES.length);
|
||||
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.
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
@@ -165,8 +190,23 @@ io.on('connection', (socket) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -47,9 +47,21 @@ export class Game {
|
||||
}
|
||||
|
||||
addPlayer(id: string, name: string): Player {
|
||||
// Asignar avatar aleatorio persistente (rebel001.jpg - rebel010.jpg)
|
||||
const avatarIdx = Math.floor(Math.random() * 10) + 1;
|
||||
const avatarStr = `rebel${avatarIdx.toString().padStart(3, '0')}.jpg`;
|
||||
// Asignar avatar aleatorio sin repetir (rebel001.jpg - rebel010.jpg)
|
||||
// Obtener avatares ya usados
|
||||
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 = {
|
||||
id,
|
||||
@@ -92,13 +104,13 @@ export class Game {
|
||||
// ... assignRoles se mantiene igual ...
|
||||
private assignRoles(goodCount: number, evilCount: number) {
|
||||
// Roles obligatorios
|
||||
const roles: Role[] = [Role.MERLIN, Role.ASSASSIN];
|
||||
const roles: Role[] = [Role.MARLENE, Role.FRANCOTIRADOR]; // Updated roles
|
||||
|
||||
// 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
|
||||
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
|
||||
const shuffledRoles = roles.sort(() => Math.random() - 0.5);
|
||||
@@ -107,10 +119,10 @@ export class Game {
|
||||
this.state.players.forEach((player, index) => {
|
||||
player.role = shuffledRoles[index];
|
||||
// Asignar facción basada en el rol
|
||||
if ([Role.MERLIN, Role.PERCIVAL, Role.LOYAL_SERVANT].includes(player.role)) {
|
||||
player.faction = Faction.RESISTANCE;
|
||||
if ([Role.MARLENE, Role.PARTISANO].includes(player.role)) { // Updated roles
|
||||
player.faction = Faction.ALIADOS;
|
||||
} 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() {
|
||||
const votes = Object.values(this.state.leaderVotes);
|
||||
const approves = votes.filter(v => v === true).length;
|
||||
const rejects = votes.filter(v => v === false).length;
|
||||
// Los nulos (timeout) no suman a ninguno, o cuentan como reject implícito?
|
||||
// "Si llega a 0... su voto no cuenta". Simplemente no suma.
|
||||
const rejects = votes.filter(v => v === false || v === null).length; // null cuenta como rechazo
|
||||
|
||||
this.log(`Votación de Líder: ${approves} A favor - ${rejects} En contra.`);
|
||||
|
||||
@@ -189,7 +210,7 @@ export class Game {
|
||||
this.state.proposedTeam = [];
|
||||
|
||||
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 {
|
||||
this.nextLeader(); // Pasa a VOTE_LEADER
|
||||
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...`);
|
||||
|
||||
// Auto-avanzar a MISSION_RESULT después de 5 segundos
|
||||
setTimeout(() => {
|
||||
this.finishReveal();
|
||||
}, 5000);
|
||||
// El cliente controlará el avance a MISSION_RESULT con su timer
|
||||
}
|
||||
|
||||
|
||||
@@ -268,7 +286,7 @@ export class Game {
|
||||
const failures = this.state.questResults.filter(r => r === false).length;
|
||||
|
||||
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) {
|
||||
this.state.phase = GamePhase.ASSASSIN_PHASE;
|
||||
this.log('¡La Resistencia ha triunfado! Pero el Asesino tiene una última oportunidad...');
|
||||
@@ -284,14 +302,14 @@ export class Game {
|
||||
|
||||
assassinKill(targetId: string) {
|
||||
const target = this.state.players.find(p => p.id === targetId);
|
||||
if (target && target.role === Role.MERLIN) {
|
||||
this.endGame(Faction.SPIES, '¡El Asesino ha eliminado a Marlenne (Merlín)!');
|
||||
if (target && target.role === Role.MARLENE) { // Updated Role
|
||||
this.endGame(Faction.ALEMANES, '¡El Asesino ha eliminado a Marlene!'); // Updated Faction and message
|
||||
} 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 nextIdx = (currentIdx + 1) % this.state.players.length;
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
export enum Role {
|
||||
// Bando del Bien (Resistencia Francesa)
|
||||
MERLIN = 'merlin', // Marlenne
|
||||
PERCIVAL = 'percival',
|
||||
LOYAL_SERVANT = 'loyal_servant', // Soldado Resistencia
|
||||
// Bando Aliado (Resistencia Francesa)
|
||||
MARLENE = 'marlene', // Agente de inteligencia (antes Merlin)
|
||||
CAPITAN_PHILIPPE = 'capitan_philippe', // Oficial que conoce a Marlene (antes Percival)
|
||||
PARTISANO = 'partisano', // Miembro leal de la resistencia (antes Loyal Servant)
|
||||
|
||||
// Bando del Mal (Ocupación Alemana)
|
||||
MORDRED = 'mordred',
|
||||
ASSASSIN = 'assassin',
|
||||
MORGANA = 'morgana',
|
||||
OBERON = 'oberon',
|
||||
MINION = 'minion', // Soldado Alemán
|
||||
// Bando Alemán (Ocupación Nazi)
|
||||
COMANDANTE_SCHMIDT = 'comandante_schmidt', // Oficial nazi oculto (antes Mordred)
|
||||
FRANCOTIRADOR = 'francotirador', // Puede eliminar a Marlene (antes Assassin)
|
||||
AGENTE_DOBLE = 'agente_doble', // Se hace pasar por Marlene (antes Morgana)
|
||||
INFILTRADO = 'infiltrado', // Espía solitario (antes Oberon)
|
||||
COLABORACIONISTA = 'colaboracionista', // Espía genérico (antes Minion/Spy)
|
||||
}
|
||||
|
||||
export enum Faction {
|
||||
RESISTANCE = 'resistance',
|
||||
SPIES = 'spies',
|
||||
ALIADOS = 'aliados', // Antes RESISTANCE
|
||||
ALEMANES = 'alemanes', // Antes SPIES
|
||||
}
|
||||
|
||||
export enum GamePhase {
|
||||
|
||||
Reference in New Issue
Block a user