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:
Resistencia Dev
2025-12-07 00:20:33 +01:00
parent 8f95413782
commit 9e0e343868
8 changed files with 177 additions and 118 deletions

View File

@@ -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',
}

View File

@@ -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' && (

View File

@@ -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()}
/>
)}

View File

@@ -4,12 +4,13 @@ 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];
if (!lastMission) {
return (
<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 }}
>
<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 }}
>
{isSuccess ? '¡MISIÓN EXITOSA!' : 'MISIÓN FALLIDA'}
</motion.h2>
<motion.div
<motion.div
className="text-white text-3xl mb-8 bg-black/50 p-6 rounded-xl"
initial={{ y: 50, opacity: 0 }}
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 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}
</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>
);
}

View File

@@ -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>