import 'package:flutter/material.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:provider/provider.dart'; import '../estado/estado_juego.dart'; import '../modelos/inicio_partida_multijugador.dart'; import '../modelos/palabra.dart'; import '../modelos/partida.dart'; import '../servicios/servicio_nearby.dart'; import '../servicios/servicio_permisos.dart'; import '../servicios/servicio_perfil_usuario.dart'; import '../tema/componentes_farolero.dart'; import '../tema/tema_app.dart'; import 'pantalla_gestor_host.dart'; import 'pantalla_lobby_host.dart'; import 'pantalla_principal.dart'; import 'pantalla_ver_palabra.dart'; class PantallaCrearPartida extends StatefulWidget { final bool modoInicial; final bool bloquearModo; const PantallaCrearPartida({ super.key, this.modoInicial = false, this.bloquearModo = false, }); @override State createState() => _PantallaCrearPartidaState(); } class _PantallaCrearPartidaState extends State { bool _modoMultimovil = false; String _categoria = 'todas'; int _numImpostores = 1; bool _pistaImpostor = false; int? _tiempoDebate; final List _jugadores = []; final _controladorNombre = TextEditingController(); bool _agregandoPerfilPendiente = false; final _opcionesTiempo = [null, 60, 120, 180, 300]; @override void initState() { super.initState(); _modoMultimovil = widget.modoInicial; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _agregarPerfilLocalSiNecesario(); }); } void _agregarPerfilLocalSiNecesario() { if (_modoMultimovil) return; final servicioPerfil = context.read(); if (!servicioPerfil.cargado) return; final perfil = servicioPerfil.perfil; final l10n = AppLocalizations.of(context)!; final nombre = perfil.nombre.trim().isEmpty ? l10n.defaultPlayerName : perfil.nombre.trim(); if (_jugadores.contains(nombre)) return; setState(() { _jugadores.insert(0, nombre); }); } int get _maxImpostores => _modoMultimovil ? 4 : (_jugadores.length / 3).floor().clamp(1, 4); List _etiquetasTiempo(AppLocalizations l10n) => [ l10n.noLimit, l10n.oneMin, l10n.twoMin, l10n.threeMin, l10n.fiveMin, ]; void _agregarJugador() { final l10n = AppLocalizations.of(context)!; final nombre = _controladorNombre.text.trim(); if (nombre.isEmpty) return; if (_jugadores.contains(nombre)) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.playerAlreadyExists))); return; } if (_jugadores.length >= 20) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.maxPlayersReached))); return; } setState(() { _jugadores.add(nombre); _controladorNombre.clear(); if (_numImpostores > _maxImpostores) { _numImpostores = _maxImpostores; } }); } void _eliminarJugador(int index) { final perfil = context.read().perfil; final l10n = AppLocalizations.of(context)!; final nombrePerfil = perfil.nombre.trim().isEmpty ? l10n.defaultPlayerName : perfil.nombre.trim(); if (index == 0 && _jugadores[index] == nombrePerfil) return; setState(() { _jugadores.removeAt(index); if (_numImpostores > _maxImpostores && _maxImpostores > 0) { _numImpostores = _maxImpostores; } }); } void _iniciarPartida() { final l10n = AppLocalizations.of(context)!; if (_modoMultimovil) { _iniciarPartidaMulti(); return; } if (_jugadores.length < 3) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.minPlayersRequired))); return; } final estado = context.read(); estado.crearPartida( config: ConfigPartida( modoMultimovil: false, categoria: _categoria, numImpostores: _numImpostores, pistaImpostor: _pistaImpostor, tiempoDebateSegundos: _tiempoDebate, ), nombresJugadores: _jugadores, ); Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const PantallaVerPalabra()), ); } Future _iniciarPartidaMulti() async { final l10n = AppLocalizations.of(context)!; // 1. Pedir permisos automáticamente final permisosOk = await ServicioPermisos.solicitarPermisosNearby(context); if (!permisosOk) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(l10n.bluetoothLocationPermissionsShort), ), ); } return; } // 2. Usar el perfil principal del dispositivo como usuario del host. final nombre = await _seleccionarUsuarioHost(); if (nombre == null || nombre.trim().isEmpty) return; // 3. Iniciar host en Nearby if (!mounted) return; final nearby = context.read(); final servicioPerfil = context.read(); final perfil = servicioPerfil.perfil; final gamificacion = servicioPerfil.resumenGamificacion; final nombreSala = '${nombre.trim()} - Farolero'; final ok = await nearby.iniciarHost( nombreSala, nombre.trim(), miNick: perfil.nick, miAvatar: perfil.avatarAsset, miFuego: gamificacion.fuego, miMedallas: gamificacion.medallas, ); if (!ok) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(l10n.couldNotCreateRoom), ), ); } return; } // 4. Navegar al lobby con QR if (mounted) { Navigator.push( context, MaterialPageRoute( builder: (_) => PantallaLobbyHost( nombreSala: nombreSala, onIniciar: () { // Cuando el host toca "Iniciar" con suficientes jugadores final estado = context.read(); final sala = nearby.estadoSala; if (sala == null) return; final validacion = sala.iniciarPartida(); if (!validacion.exitoso) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( l10n.cannotStartWithReason( validacion.codigo ?? l10n.invalidRoom, ), ), ), ); return; } estado.crearPartidaDesdeSala( config: ConfigPartida( modoMultimovil: true, categoria: _categoria, numImpostores: _numImpostores, pistaImpostor: _pistaImpostor, tiempoDebateSegundos: _tiempoDebate, ), sala: sala, ); final partida = estado.partida!; final asignaciones = partida.jugadores.map((jugador) { final usuarioSala = sala.usuarios[jugador.id]; final clientId = usuarioSala?.clienteIdSeleccionado; final cliente = clientId == null ? null : sala.clientes[clientId]; return AsignacionJugador( jugadorId: jugador.id, nombre: jugador.nombre, clientId: clientId ?? sala.hostClientId, endpointId: cliente?.endpointId, ); }).toList(); final impostores = { for (final jugador in partida.jugadores) jugador.id: jugador.esImpostor, }; final jugadoresTodos = partida.jugadores .map( (jugador) => { 'id': jugador.id, 'nombre': jugador.nombre, 'eliminado': jugador.eliminado, }, ) .toList(); nearby.enviarInicioPartidaMulti( asignaciones: asignaciones, palabraSecreta: partida.palabraSecreta, categoria: _categoria, impostoresPorJugadorId: impostores, jugadoresTodos: jugadoresTodos, ); Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => PantallaGestorHost( onPartidaFin: () { estado.limpiar(); Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => const PantallaPrincipal(), ), ); }, ), ), ); }, ), ), ); } } /// Devuelve el perfil principal del dispositivo para crear la sala. Future _seleccionarUsuarioHost() async { return context.read().perfil.nombre; } @override void dispose() { _controladorNombre.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final estado = context.watch(); final servicioPerfil = context.watch(); final categorias = ['todas', ...?estado.banco?.nombresCategorias]; final etiquetas = _etiquetasTiempo(l10n); final nombrePerfilActual = servicioPerfil.perfil.nombre.trim().isEmpty ? l10n.defaultPlayerName : servicioPerfil.perfil.nombre.trim(); if (!_modoMultimovil && servicioPerfil.cargado && !_agregandoPerfilPendiente && !_jugadores.contains(nombrePerfilActual)) { _agregandoPerfilPendiente = true; WidgetsBinding.instance.addPostFrameCallback((_) { _agregandoPerfilPendiente = false; if (mounted) _agregarPerfilLocalSiNecesario(); }); } return Scaffold( appBar: AppBar(title: Text(l10n.createGame)), body: FondoFarolero( intenso: true, child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(18, 18, 18, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _CrearPartidaHeader( titulo: l10n.createGame, subtitulo: l10n.playersRange, ), const SizedBox(height: 12), if (!widget.bloquearModo) ...[ // Modo de juego Card( child: Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.gameMode, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), SegmentedButton( segments: [ ButtonSegment( value: false, label: Text(l10n.singleDevice), icon: const Icon(Icons.phone_android), ), ButtonSegment( value: true, label: Text(l10n.multiDevice), icon: const Icon(Icons.devices), ), ], selected: {_modoMultimovil}, onSelectionChanged: (valor) { setState(() { _modoMultimovil = valor.first; if (_numImpostores > _maxImpostores) { _numImpostores = _maxImpostores; } }); _agregarPerfilLocalSiNecesario(); }, ), ], ), ), ), const SizedBox(height: 12), ] else ...[ PanelFarolero( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ Icon( _modoMultimovil ? Icons.devices : Icons.phone_android, color: TemaApp.colorNaranja, ), const SizedBox(width: 12), Expanded( child: Text( _modoMultimovil ? l10n.multiDeviceGameLabel : l10n.singleDeviceGameLabel, style: Theme.of(context).textTheme.titleMedium, ), ), ], ), ), const SizedBox(height: 12), ], // Categoría Card( child: Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.category, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: DropdownButtonFormField( initialValue: _categoria, decoration: const InputDecoration( prefixIcon: Icon(Icons.category), ), items: categorias.map((c) { return DropdownMenuItem( value: c, child: Text( BancoPalabras.nombreBonitoCategoria(c, l10n), ), ); }).toList(), onChanged: (v) => setState(() => _categoria = v!), ), ), ], ), ), ), const SizedBox(height: 12), if (!_modoMultimovil) ...[ // Jugadores Card( child: Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.playersCount(_jugadores.length), style: Theme.of(context).textTheme.titleLarge, ), Text( l10n.playersRangeHint, style: Theme.of(context).textTheme.bodyMedium, ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _controladorNombre, decoration: InputDecoration( hintText: l10n.playerNameHint, prefixIcon: const Icon(Icons.person_add), ), textCapitalization: TextCapitalization.words, onSubmitted: (_) => _agregarJugador(), ), ), const SizedBox(width: 8), IconButton.filled( onPressed: _agregarJugador, icon: const Icon(Icons.add), ), ], ), const SizedBox(height: 8), ..._jugadores.asMap().entries.map((e) { final perfil = servicioPerfil.perfil; final nombrePerfil = perfil.nombre.trim().isEmpty ? l10n.defaultPlayerName : perfil.nombre.trim(); final inicialPerfil = nombrePerfil.isEmpty ? '?' : nombrePerfil.substring(0, 1).toUpperCase(); final esPerfilLocal = e.key == 0 && e.value == nombrePerfil; return ListTile( minLeadingWidth: 62, leading: esPerfilLocal ? SizedBox( width: 62, height: 62, child: AvatarFarolero( texto: inicialPerfil, assetPath: perfil.avatarAsset, size: 52, ), ) : CircleAvatar( backgroundColor: TemaApp.colorTarjeta, child: Text( '${e.key + 1}', style: const TextStyle(color: TemaApp.colorTexto), ), ), title: Text(e.value), subtitle: esPerfilLocal ? Text(l10n.mainDeviceUser) : null, trailing: esPerfilLocal ? const Icon(Icons.lock, color: TemaApp.colorDorado) : IconButton( icon: const Icon( Icons.close, color: TemaApp.colorAcento, ), onPressed: () => _eliminarJugador(e.key), ), ); }), ], ), ), ), const SizedBox(height: 12), ], // Configuración de partida Card( child: Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.configuration, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), // Número de impostores Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.impostors), Row( children: [ IconButton( onPressed: _numImpostores > 1 ? () => setState(() => _numImpostores--) : null, icon: const Icon(Icons.remove_circle_outline), ), Text( '$_numImpostores', style: Theme.of(context).textTheme.titleLarge, ), IconButton( onPressed: _numImpostores < _maxImpostores ? () => setState(() => _numImpostores++) : null, icon: const Icon(Icons.add_circle_outline), ), ], ), ], ), // Pista para impostor SwitchListTile( title: Text(l10n.impostorClue), subtitle: Text(l10n.impostorClueDescription), value: _pistaImpostor, onChanged: (v) => setState(() => _pistaImpostor = v), contentPadding: EdgeInsets.zero, ), // Temporizador Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.debateTime), DropdownButton( value: _tiempoDebate, items: List.generate( _opcionesTiempo.length, (i) => DropdownMenuItem( value: _opcionesTiempo[i], child: Text(etiquetas[i]), ), ), onChanged: (v) => setState(() => _tiempoDebate = v), ), ], ), ], ), ), ), const SizedBox(height: 24), // Botón iniciar BotonFarolero( texto: l10n.startGame, icono: Icons.play_arrow, onPressed: (_modoMultimovil || _jugadores.length >= 3) ? _iniciarPartida : null, ), const SizedBox(height: 16), ], ), ), ), ); } } class _CrearPartidaHeader extends StatelessWidget { final String titulo; final String subtitulo; const _CrearPartidaHeader({ required this.titulo, required this.subtitulo, }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final ancho = constraints.maxWidth.clamp(320.0, 720.0).toDouble(); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: ancho), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AspectRatio( aspectRatio: 2, child: Image.asset( 'assets/ui/generated/create_game/create_game_header_art.png', fit: BoxFit.contain, opacity: const AlwaysStoppedAnimation(0.96), ), ), const SizedBox(height: 8), Text( titulo, textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: TemaApp.colorDorado, fontWeight: FontWeight.w900, shadows: [ Shadow( color: TemaApp.colorNaranja.withValues(alpha: 0.55), blurRadius: 16, ), ], ), ), const SizedBox(height: 4), Text( subtitulo, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: TemaApp.colorTextoSecundario, ), ), ], ), ), ); }, ); } }