NUEVA GESTIÓN DE USUARIOS Y PARTIDAS

This commit is contained in:
2026-05-09 16:23:55 +02:00
parent f64f36b78f
commit a5d24c2721
11 changed files with 606 additions and 81 deletions

View File

@@ -16,7 +16,14 @@ import 'pantalla_principal.dart';
import 'pantalla_ver_palabra.dart';
class PantallaCrearPartida extends StatefulWidget {
const PantallaCrearPartida({super.key});
final bool modoInicial;
final bool bloquearModo;
const PantallaCrearPartida({
super.key,
this.modoInicial = false,
this.bloquearModo = false,
});
@override
State<PantallaCrearPartida> createState() => _PantallaCrearPartidaState();
@@ -33,7 +40,14 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
final _opcionesTiempo = <int?>[null, 60, 120, 180, 300];
int get _maxImpostores => (_jugadores.length / 3).floor().clamp(1, 4);
@override
void initState() {
super.initState();
_modoMultimovil = widget.modoInicial;
}
int get _maxImpostores =>
_modoMultimovil ? 4 : (_jugadores.length / 3).floor().clamp(1, 4);
List<String> _etiquetasTiempo(AppLocalizations l10n) => [
l10n.noLimit,
@@ -295,42 +309,70 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
),
),
const SizedBox(height: 12),
// Modo de juego
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
if (!widget.bloquearModo) ...[
// Modo de juego
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.gameMode,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
SegmentedButton<bool>(
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;
}
});
},
),
],
),
),
),
const SizedBox(height: 12),
] else ...[
PanelFarolero(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Text(
l10n.gameMode,
style: Theme.of(context).textTheme.titleLarge,
Icon(
_modoMultimovil ? Icons.devices : Icons.phone_android,
color: TemaApp.colorNaranja,
),
const SizedBox(height: 12),
SegmentedButton<bool>(
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);
},
const SizedBox(width: 12),
Expanded(
child: Text(
_modoMultimovil
? 'Partida multidispositivo'
: 'Partida en este dispositivo',
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
),
),
const SizedBox(height: 12),
const SizedBox(height: 12),
],
// Categoría
Card(
child: Padding(
@@ -367,13 +409,14 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
),
const SizedBox(height: 12),
// Jugadores
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!_modoMultimovil) ...[
// Jugadores
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -429,11 +472,12 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
dense: true,
);
}),
],
],
),
),
),
),
const SizedBox(height: 12),
const SizedBox(height: 12),
],
// Configuración de partida
Card(

View File

@@ -4,13 +4,14 @@ import 'package:provider/provider.dart';
import '../modelos/inicio_partida_multijugador.dart';
import '../modelos/palabra.dart';
import '../modelos/snapshot_partida_online.dart';
import '../servicios/servicio_historial_partidas.dart';
import '../servicios/servicio_nearby.dart';
import '../tema/tema_app.dart';
import 'pantalla_notas_online.dart';
import 'pantalla_principal.dart';
import 'pantalla_revision_palabra.dart';
class PantallaFinPartidaOnline extends StatelessWidget {
class PantallaFinPartidaOnline extends StatefulWidget {
final SnapshotPartidaOnline snapshot;
final List<JugadorInicioPartida> jugadoresControlados;
final String? pistaCategoria;
@@ -22,11 +23,33 @@ class PantallaFinPartidaOnline extends StatelessWidget {
this.pistaCategoria,
});
@override
State<PantallaFinPartidaOnline> createState() =>
_PantallaFinPartidaOnlineState();
}
class _PantallaFinPartidaOnlineState extends State<PantallaFinPartidaOnline> {
bool _guardada = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final snapshot = widget.snapshot;
final jugadoresControlados = widget.jugadoresControlados;
final pistaCategoria = widget.pistaCategoria;
final ganaronJugadores = snapshot.ganador == 'jugadores';
if (!_guardada && snapshot.ganador != null) {
_guardada = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context
.read<ServicioHistorialPartidas>()
.guardarSnapshotOnline(snapshot);
}
});
}
return Scaffold(
appBar: AppBar(
title: Text(l10n.gameOver),

View File

@@ -8,6 +8,7 @@ import '../modelos/inicio_partida_multijugador.dart';
import '../modelos/jugador.dart';
import '../modelos/partida.dart';
import '../modelos/snapshot_partida_online.dart';
import '../servicios/servicio_historial_partidas.dart';
import '../servicios/servicio_nearby.dart';
import '../tema/componentes_farolero.dart';
import '../tema/tema_app.dart';
@@ -29,6 +30,7 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
Timer? _timer;
int _segundosRestantes = 0;
bool _hostListo = false;
bool _partidaOnlineGuardada = false;
String? _primerTurnoId;
String? _primerTurnoNombre;
final Map<String, bool> _clientesListos = {};
@@ -200,6 +202,7 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildAvisoClientesDesconectados(context, nearby),
_buildFaseIndicator(context, partida.fase, l10n),
const SizedBox(height: 16),
Expanded(
@@ -226,6 +229,58 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
);
}
Widget _buildAvisoClientesDesconectados(
BuildContext context,
ServicioNearby nearby,
) {
final sala = nearby.estadoSala;
final usuariosAfectados = sala?.usuariosDeClientesDesconectados ?? const [];
if (usuariosAfectados.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: TemaApp.decoracionPanel(
color: TemaApp.colorAcento.withValues(alpha: 0.16),
borderColor: TemaApp.colorAcento.withValues(alpha: 0.65),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.link_off, color: TemaApp.colorAcento),
const SizedBox(width: 8),
Expanded(
child: Text(
'Hay jugadores con el dispositivo desconectado.',
style: Theme.of(context).textTheme.titleSmall,
),
),
],
),
const SizedBox(height: 6),
Text(
usuariosAfectados.map((usuario) => usuario.nombre).join(', '),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton.icon(
onPressed: () => nearby.asumirUsuariosDesconectados(),
icon: const Icon(Icons.person_add_alt_1),
label: const Text('Asumir en este móvil'),
),
),
],
),
),
);
}
Widget _buildFaseIndicator(
BuildContext context,
FaseJuego fase,
@@ -1115,12 +1170,21 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
estado.comprobarFinPartida();
await nearby.enviarFinPartida(
_snapshot(fase: 'finPartida', revelarFinal: true).toJson(),
);
final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true)
.toJson();
await _guardarHistorialOnlineHost(context);
await nearby.enviarFinPartida(snapshotFinal);
if (mounted) setState(() {});
}
Future<void> _guardarHistorialOnlineHost(BuildContext context) async {
if (_partidaOnlineGuardada) return;
final partida = context.read<EstadoJuego>().partida;
if (partida?.ganador == null) return;
_partidaOnlineGuardada = true;
await context.read<ServicioHistorialPartidas>().guardarPartida(partida!);
}
Future<void> _iniciarAdivinanzaOnline(BuildContext context) async {
final estado = context.read<EstadoJuego>();
final nearby = context.read<ServicioNearby>();
@@ -1170,9 +1234,10 @@ class _PantallaGestorHostState extends State<PantallaGestorHost> {
final acierto = estado.intentarAdivinar(intento);
if (acierto) {
final nearby = context.read<ServicioNearby>();
await nearby.enviarFinPartida(
_snapshot(fase: 'finPartida', revelarFinal: true).toJson(),
);
final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true)
.toJson();
await _guardarHistorialOnlineHost(context);
await nearby.enviarFinPartida(snapshotFinal);
if (mounted) setState(() {});
return;
}

View File

@@ -5,7 +5,7 @@ import '../servicios/servicio_perfil_usuario.dart';
import '../tema/componentes_farolero.dart';
import '../tema/tema_app.dart';
import 'pantalla_ajustes.dart';
import 'pantalla_crear_partida.dart';
import 'pantalla_seleccion_modo_juego.dart';
import 'pantalla_historial.dart';
import 'pantalla_reglas.dart';
import 'pantalla_unirse.dart';
@@ -94,7 +94,7 @@ class PantallaPrincipal extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PantallaCrearPartida(),
builder: (_) => const PantallaSeleccionModoJuego(),
),
);
},
@@ -142,30 +142,6 @@ class PantallaPrincipal extends StatelessWidget {
},
),
),
const SizedBox(width: 8),
Expanded(
child: AccesoFarolero(
etiqueta: 'Logros',
icono: Icons.emoji_events,
onPressed: () {},
),
),
const SizedBox(width: 8),
Expanded(
child: AccesoFarolero(
etiqueta: 'Ranking',
icono: Icons.bar_chart,
onPressed: () {},
),
),
const SizedBox(width: 8),
Expanded(
child: AccesoFarolero(
etiqueta: 'Tienda',
icono: Icons.storefront,
onPressed: () {},
),
),
],
),
const SizedBox(height: 28),

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import '../tema/componentes_farolero.dart';
import '../tema/tema_app.dart';
import 'pantalla_crear_partida.dart';
class PantallaSeleccionModoJuego extends StatelessWidget {
const PantallaSeleccionModoJuego({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Elegir modo de juego')),
body: FondoFarolero(
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(
Icons.sports_esports,
size: 64,
color: TemaApp.colorNaranja,
),
const SizedBox(height: 16),
Text(
'¿Cómo querés jugar?',
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Elegí primero el tipo de partida para configurar solo lo necesario.',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 28),
_ModoCard(
icono: Icons.phone_android,
titulo: 'Partida en este dispositivo',
descripcion:
'Todos los jugadores usan este móvil. Acá se agregan los nombres manualmente.',
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PantallaCrearPartida(
modoInicial: false,
bloquearModo: true,
),
),
),
),
const SizedBox(height: 14),
_ModoCard(
icono: Icons.devices,
titulo: 'Partida multidispositivo',
descripcion:
'Este móvil crea el servidor. Los usuarios se gestionan después en el lobby.',
destacado: true,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PantallaCrearPartida(
modoInicial: true,
bloquearModo: true,
),
),
),
),
],
),
),
),
),
),
),
);
}
}
class _ModoCard extends StatelessWidget {
final IconData icono;
final String titulo;
final String descripcion;
final bool destacado;
final VoidCallback onTap;
const _ModoCard({
required this.icono,
required this.titulo,
required this.descripcion,
required this.onTap,
this.destacado = false,
});
@override
Widget build(BuildContext context) {
final color = destacado ? TemaApp.colorNaranja : TemaApp.colorAcento;
return Card(
color: TemaApp.colorTarjeta,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withValues(alpha: 0.7)),
),
child: Icon(icono, color: color, size: 30),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(titulo, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 6),
Text(
descripcion,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(width: 8),
const Icon(Icons.chevron_right),
],
),
),
),
);
}
}

View File

@@ -314,7 +314,12 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
if (!mounted) return;
final nearby = context.read<ServicioNearby>();
final ok = await nearby.buscarHosts(_nombreController.text.trim());
final perfil = context.read<ServicioPerfilUsuario>().perfil;
final ok = await nearby.buscarHosts(
_nombreController.text.trim(),
miNick: perfil.nick,
miAvatar: perfil.avatarAsset,
);
if (ok) {
setState(() {
@@ -337,11 +342,14 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
});
final nearby = context.read<ServicioNearby>();
final perfil = context.read<ServicioPerfilUsuario>().perfil;
// Parar discovery antes de conectar
await nearby.pararBusqueda();
final ok = await nearby.conectarAHost(
endpointId,
_nombreController.text.trim(),
miNick: perfil.nick,
miAvatar: perfil.avatarAsset,
);
if (!ok && mounted) {
@@ -381,7 +389,12 @@ class _PantallaUnirseState extends State<PantallaUnirse> {
// Iniciar búsqueda para que Nearby encuentre al host
final nearby = context.read<ServicioNearby>();
if (!nearby.buscando) {
await nearby.buscarHosts(_nombreController.text.trim());
final perfil = context.read<ServicioPerfilUsuario>().perfil;
await nearby.buscarHosts(
_nombreController.text.trim(),
miNick: perfil.nick,
miAvatar: perfil.avatarAsset,
);
}
return;
}