From eb2662f561eda5854f6b5b511ba88bb1b2c8f404 Mon Sep 17 00:00:00 2001 From: ShanaiaBot Date: Wed, 15 Apr 2026 02:09:05 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20multidispositivo=20-=20Random=20seguro?= =?UTF-8?q?=20+=20gestor=20host=20+=20reacci=C3=B3n=20clientes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Random.secure() para selección de impostores (no predecible) - Random.secure() también en desempate de votación - Nueva PantallaGestorHost para coordinación multi-device - Navegación: host va a gestor tras iniciar, no a pantalla de palabra - PantallaPalabraCliente: cada jugador ve su palabra en su móvil - PantallaDebateCliente: debate con timer y botón solicitar votación - PantallaVotacionCliente: voto desde el móvil del cliente - PantallaUnirse: listener que reacciona a partidaInicio y cambia de fase - Protocolo: listo/voto/solicitoVotacion via Nearby hacia el host - Nuevas cadenas l10n ES --- SPEC.md | 196 +++++++ lib/estado/estado_juego.dart | 39 +- lib/l10n/app_es.arb | 24 +- lib/l10n/generated/app_localizations.dart | 90 ++++ lib/l10n/generated/app_localizations_ar.dart | 48 ++ lib/l10n/generated/app_localizations_ca.dart | 48 ++ lib/l10n/generated/app_localizations_de.dart | 48 ++ lib/l10n/generated/app_localizations_en.dart | 48 ++ lib/l10n/generated/app_localizations_es.dart | 48 ++ lib/l10n/generated/app_localizations_eu.dart | 48 ++ lib/l10n/generated/app_localizations_fr.dart | 48 ++ lib/l10n/generated/app_localizations_hi.dart | 48 ++ lib/l10n/generated/app_localizations_it.dart | 48 ++ lib/l10n/generated/app_localizations_ja.dart | 48 ++ lib/l10n/generated/app_localizations_ko.dart | 48 ++ lib/l10n/generated/app_localizations_nl.dart | 48 ++ lib/l10n/generated/app_localizations_pl.dart | 48 ++ lib/l10n/generated/app_localizations_pt.dart | 48 ++ lib/l10n/generated/app_localizations_ru.dart | 48 ++ lib/l10n/generated/app_localizations_tr.dart | 48 ++ lib/l10n/generated/app_localizations_zh.dart | 48 ++ lib/pantallas/pantalla_crear_partida.dart | 118 +++-- lib/pantallas/pantalla_debate_cliente.dart | 156 ++++++ lib/pantallas/pantalla_gestor_host.dart | 514 +++++++++++++++++++ lib/pantallas/pantalla_palabra_cliente.dart | 169 ++++++ lib/pantallas/pantalla_unirse.dart | 108 ++++ lib/pantallas/pantalla_votacion_cliente.dart | 112 ++++ 27 files changed, 2282 insertions(+), 60 deletions(-) create mode 100644 SPEC.md create mode 100644 lib/pantallas/pantalla_debate_cliente.dart create mode 100644 lib/pantallas/pantalla_gestor_host.dart create mode 100644 lib/pantallas/pantalla_palabra_cliente.dart create mode 100644 lib/pantallas/pantalla_votacion_cliente.dart diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..d83807c --- /dev/null +++ b/SPEC.md @@ -0,0 +1,196 @@ +# Farolero — SDD: Multidispositivo completo + Impostor seguro + +## 1. Explore + +### Hallazgos + +**Bug 1: El host funciona como juego en un solo móvil** + +- En `pantalla_crear_partida.dart` → `_iniciarPartidaMulti()`, al pulsar "Iniciar": + 1. Crea la partida localmente ✅ + 2. Envía palabras a clientes via Nearby ✅ + 3. Navega a `PantallaVerPalabra` ❌ +- `PantallaVerPalabra` está diseñada para juego en un solo móvil (muestra TODOS los jugadores en lista) +- El host no necesita ver palabras — es el gestor, no un jugador + +**Bug 2: Clientes no se enteran de que el juego ha started** + +- `ServicioNearby` procesa `partidaInicio` y actualiza `_palabraRecibida` / `_soyImpostor` ✅ +- Llama a `notifyListeners()` ✅ +- Pero `PantallaUnirse` → `_buildPantallaEspera` no escucha cambios de `ServicioNearby` +- Se queda infinamente en "Esperando al host..." aunque la partida ya haya started + +**Bug 3: Random de impostores predecible** + +- `EstadoJuego.crearPartida()` usa `Random()` con seed del sistema (determinista) +- `indices.shuffle(rng)` mezcla un array [0,1,2,...,n-1] — la mezcla depende del estado inicial +- Si un atacante conoce el número de jugadores y el momento de creación, puede reconstruir la seed +- Solución: usar `Random.secure()` (CSPRNG) o al menos `Random.fromSecureRandom()` + +### Restricciones + +- Nearby Connections tiene límite de payloads (4KB por mensaje) +- Google Nearby Connections requiere Bluetooth + Location permissions en Android/iOS +- La palabra secreta NO debe enviarse en texto plano a la red ( Nearby es inseguro en teoría — cualquier dispositivo puede sniffear) +- Compatibilidad hacia atrás: modo un solo móvil no debe romperse + +--- + +## 2. Propose + +### Cambio 1: Pantalla Gestor Host (multidispositivo) + +El host en modo multi-device necesita una pantalla que: +- Muestre la lista de jugadores conectados y sus estados (listo para debate/votación) +- Permita avanzar de fase (debate → votación → resultado) +- NO muestre palabras a nadie +- Gestione la coordinación de la partida completa + +**Pantallas afectadas/creadas:** +- Nueva: `pantalla_gestor_host.dart` +- Modificar: `pantalla_crear_partida.dart` (onIniciar → navegar a gestor) +- Modificar: `pantalla_lobby_host.dart` (el QR se queda ahí) + +### Cambio 2: Reacción de clientes al inicio de partida + +Los clientes deben: +- Escuchar cambios en `ServicioNearby` (registrar listener) +- Cuando llega `partidaInicio`, navegar automáticamente a la pantalla de ver su palabra +- Crear pantalla dedicada para cliente: `pantalla_palabra_cliente.dart` + +**Pantallas afectadas/creadas:** +- Nueva: `pantalla_palabra_cliente.dart` +- Nueva: `pantalla_debate_cliente.dart` +- Nueva: `pantalla_votacion_cliente.dart` +- Modificar: `pantalla_unirse.dart` (registrar listener y reaccionar) + +### Cambio 3: Impostor con Random seguro + +Reemplazar `Random()` por `Random.secure()` en `EstadoJuego.crearPartida()`. + +**Archivos afectados:** +- `lib/estado/estado_juego.dart` + +--- + +## 3. Spec + +### 3.1 PantallaGestorHost + +**Props:** +```dart +class PantallaGestorHost extends StatelessWidget { + final VoidCallback onPartidaFin; +} +``` + +**Responsabilidades:** +- Escuchar cambios de `EstadoJuego` (fase actual) +- Escuchar cambios de `ServicioNearby` (desconexiones) +- Botones para avanzar de fase +- Mostrar estado de cada jugador (en debate, votando, eliminado) +- NO mostrar palabras a nadie + +**Estados:** +- `debate`: muestra timer si hay tiempo configurado, botón "Iniciar votación" +- `votacion`: muestra quién ha votado, botón "Ver resultados" cuando todos voted +- `resultado`: muestra quién fue eliminado y si era impostor +- `adivinanza`: el impostor剩余 intenta adivinar +- `finPartida`: muestra ganador + +### 3.2 PantallaPalabraCliente + +**Props:** +```dart +class PantallaPalabraCliente extends StatelessWidget { + final String palabra; + final bool esImpostor; + final String? pistaCategoria; + final VoidCallback onVisto; +} +``` + +**Responsabilidades:** +- Mostrar la palabra solo al jugador correspondiente +- Si es impostor: mostrar pista de categoría (si está activa) +- Mantener pulsado para revelar (mismo UX que `_PantallaRevelarPalabra`) +- Al pulsar "Ya la he visto" → enviar `listo` al host via Nearby +- Esperar al host que inicie el debate + +### 3.3 Sincronización de fases + +**Protocolo de mensajes Via Nearby:** + +``` +HOST → CLIENTE: + partidaInicio → { palabra, esImpostor, categoria, numJugadores } + fase → { fase: "debate"|"votacion"|"resultado"|"adivinanza"|"fin" } + votacionResultado → { eliminadoId, eliminadoNombre, eraImpostor, votos } + impostorAdivina → { acierto: bool } + partidaFin → { ganador: "jugadores"|"impostores" } + +CLIENTE → HOST: + listo → {} (el cliente ha visto su palabra y está listo) + voto → { votoporId } +``` + +### 3.4 Random seguro para impostores + +```dart +// ANTES (predecible): +final rng = Random(); +final indices = List.generate(jugadores.length, (i) => i); +indices.shuffle(rng); + +// DESPUÉS (seguro): +final rng = Random.secure(); +final impostoresElegidos = {}; +while (impostoresElegidos.length < numImpostores) { + impostoresElegidos.add(rng.nextInt(jugadores.length)); +} +``` + +--- + +## 4. Tasks + +### Epic 1: Impostor seguro +- [ ] **Task 1.1**: Cambiar `Random()` por `Random.secure()` en `EstadoJuego.crearPartida()` + +### Epic 2: PantallaGestorHost +- [ ] **Task 2.1**: Crear `pantalla_gestor_host.dart` con estructura de fases +- [ ] **Task 2.2**: Implementar transición `lobby → gestor` en `pantalla_crear_partida.dart` +- [ ] **Task 2.3**: Implementar envío de cambio de fase a clientes +- [ ] **Task 2.4**: Gestionar desconexiones de jugadores durante la partida + +### Epic 3: Cliente recibe inicio de partida +- [ ] **Task 3.1**: Registrar listener en `PantallaUnirse` para `partidaInicio` +- [ ] **Task 3.2**: Crear `pantalla_palabra_cliente.dart` (similar a `_PantallaRevelarPalabra` pero StatelessWidget) +- [ ] **Task 3.3**: Enviar mensaje `listo` al host cuando el cliente confirma que ha visto la palabra + +### Epic 4: Cliente en fases de juego +- [ ] **Task 4.1**: Crear `pantalla_debate_cliente.dart` (muestra countdown, botón para solicitar votación) +- [ ] **Task 4.2**: Crear `pantalla_votacion_cliente.dart` (votar desde el móvil del cliente) +- [ ] **Task 4.3**: Cliente reacciona a `fase: votacion` y muestra pantalla de voto +- [ ] **Task 4.4**: Cliente reacciona a `fase: resultado` y muestra quién fue eliminado +- [ ] **Task 4.5**: Si el cliente es impostor剩余, mostrar pantalla de adivinanza + +### Epic 5: Testing +- [ ] **Task 5.1**: Probar flujo completo multi-device (3+ jugadores) +- [ ] **Task 5.2**: Probar desconexión de un jugador durante la partida +- [ ] **Task 5.3**: Probar que en modo un solo móvil no se rompe nada + +--- + +## 5. Apply + +*(Ejecución por agente OpenCode)* + +--- + +## 6. Verify + +- [ ] Flujo multi-device: host inicia → clientes ven sus palabras → debate → votan → resultado +- [ ] Modo un solo móvil sigue funcionando exactamente igual que antes +- [ ] Impostores se seleccionan con Random.secure() — no reproducibles +- [ ] Sin regresiones en localized strings diff --git a/lib/estado/estado_juego.dart b/lib/estado/estado_juego.dart index 8caef26..179b3aa 100644 --- a/lib/estado/estado_juego.dart +++ b/lib/estado/estado_juego.dart @@ -33,8 +33,6 @@ class EstadoJuego extends ChangeNotifier { if (_banco == null) return; if (nombresJugadores.length < 3) return; - final rng = Random(); - // Seleccionar palabra final palabra = _banco!.palabraAleatoria(config.categoria); final categoriaReal = @@ -42,19 +40,18 @@ class EstadoJuego extends ChangeNotifier { // Crear jugadores final jugadores = nombresJugadores.asMap().entries.map((e) { - return Jugador( - id: 'j${e.key}', - nombre: e.value, - ); + return Jugador(id: 'j${e.key}', nombre: e.value); }).toList(); - // Asignar impostores aleatoriamente - final indices = List.generate(jugadores.length, (i) => i); - indices.shuffle(rng); - final numImpostores = - config.numImpostores.clamp(1, jugadores.length ~/ 3); - for (int i = 0; i < numImpostores; i++) { - jugadores[indices[i]].esImpostor = true; + // Asignar impostores usando Random seguro (no predecible) + final rng = Random.secure(); + final numImpostores = config.numImpostores.clamp(1, jugadores.length ~/ 3); + final impostoresElegidos = {}; + while (impostoresElegidos.length < numImpostores) { + impostoresElegidos.add(rng.nextInt(jugadores.length)); + } + for (final i in impostoresElegidos) { + jugadores[i].esImpostor = true; } // Asignar palabras @@ -124,15 +121,17 @@ class EstadoJuego extends ChangeNotifier { // Encontrar máximo final maxVotos = conteo.values.reduce(max); - final masVotados = - conteo.entries.where((e) => e.value == maxVotos).toList(); + final masVotados = conteo.entries + .where((e) => e.value == maxVotos) + .toList(); - // En caso de empate, elegir aleatoriamente - final rng = Random(); - final eliminadoId = - masVotados[rng.nextInt(masVotados.length)].key; + // En caso de empate, elegir aleatoriamente (usar Random.secure para consistencia) + final rng = Random.secure(); + final eliminadoId = masVotados[rng.nextInt(masVotados.length)].key; - final eliminado = _partida!.jugadores.firstWhere((j) => j.id == eliminadoId); + final eliminado = _partida!.jugadores.firstWhere( + (j) => j.id == eliminadoId, + ); eliminado.eliminado = true; final resultado = ResultadoVotacion( diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 922c05d..b023446 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -40,6 +40,7 @@ "impostors": "🎭 Impostores", "impostorClue": "🔍 Pista para impostor", "impostorClueDescription": "El impostor conoce la categoría", + "debate": "🗣️ Debate", "debateTime": "⏱️ Tiempo de debate", "noLimit": "Sin límite", "oneMin": "1 min", @@ -232,6 +233,11 @@ "licenses": "Licencias", "scanToJoin": "Escanea el QR para unirte", "connectedPlayers": "Jugadores conectados", + "hostGame": "Gestor de partida", + "waitingPlayersSeeWord": "Esperando que todos vean su palabra...", + "activePlayers": "Jugadores activos", + "playersVoted": "Han votado", + "waitingVoting": "Esperando que voten...", "waitingForPlayers": "Esperando jugadores...", "needMorePlayers": "Faltan {count} jugadores más", "@needMorePlayers": { @@ -255,5 +261,21 @@ "searchingGames": "Buscando partidas cercanas...", "noGamesFound": "No se encontraron partidas", "noGamesFoundHint": "Asegúrate de que el host tiene la sala abierta y estáis cerca", - "orScanQR": "¿No aparece? Escanea el QR del host" + "orScanQR": "¿No aparece? Escanea el QR del host", + "iveSeenIt": "Ya la he visto", + "clueIs": "La pista es: {category}", + "@clueIs": { + "placeholders": { + "category": { + "type": "String" + } + } + }, + "debatePhaseActive": "Fase de debate activa", + "debateInstructions": "Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.", + "solicitarVotacion": "Solicitar votación", + "votacionSolicitada": "Votación solicitada", + "whoDoYouThinkIsTheImpostor": "¿Quién es el impostor?", + "selectOnePlayer": "Selecciona a un jugador para votar", + "votar": "Votar" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 2352270..7d5b63e 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -327,6 +327,12 @@ abstract class AppLocalizations { /// **'El impostor conoce la categoría'** String get impostorClueDescription; + /// No description provided for @debate. + /// + /// In es, this message translates to: + /// **'🗣️ Debate'** + String get debate; + /// No description provided for @debateTime. /// /// In es, this message translates to: @@ -1005,6 +1011,36 @@ abstract class AppLocalizations { /// **'Jugadores conectados'** String get connectedPlayers; + /// No description provided for @hostGame. + /// + /// In es, this message translates to: + /// **'Gestor de partida'** + String get hostGame; + + /// No description provided for @waitingPlayersSeeWord. + /// + /// In es, this message translates to: + /// **'Esperando que todos vean su palabra...'** + String get waitingPlayersSeeWord; + + /// No description provided for @activePlayers. + /// + /// In es, this message translates to: + /// **'Jugadores activos'** + String get activePlayers; + + /// No description provided for @playersVoted. + /// + /// In es, this message translates to: + /// **'Han votado'** + String get playersVoted; + + /// No description provided for @waitingVoting. + /// + /// In es, this message translates to: + /// **'Esperando que voten...'** + String get waitingVoting; + /// No description provided for @waitingForPlayers. /// /// In es, this message translates to: @@ -1106,6 +1142,60 @@ abstract class AppLocalizations { /// In es, this message translates to: /// **'¿No aparece? Escanea el QR del host'** String get orScanQR; + + /// No description provided for @iveSeenIt. + /// + /// In es, this message translates to: + /// **'Ya la he visto'** + String get iveSeenIt; + + /// No description provided for @clueIs. + /// + /// In es, this message translates to: + /// **'La pista es: {category}'** + String clueIs(String category); + + /// No description provided for @debatePhaseActive. + /// + /// In es, this message translates to: + /// **'Fase de debate activa'** + String get debatePhaseActive; + + /// No description provided for @debateInstructions. + /// + /// In es, this message translates to: + /// **'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'** + String get debateInstructions; + + /// No description provided for @solicitarVotacion. + /// + /// In es, this message translates to: + /// **'Solicitar votación'** + String get solicitarVotacion; + + /// No description provided for @votacionSolicitada. + /// + /// In es, this message translates to: + /// **'Votación solicitada'** + String get votacionSolicitada; + + /// No description provided for @whoDoYouThinkIsTheImpostor. + /// + /// In es, this message translates to: + /// **'¿Quién es el impostor?'** + String get whoDoYouThinkIsTheImpostor; + + /// No description provided for @selectOnePlayer. + /// + /// In es, this message translates to: + /// **'Selecciona a un jugador para votar'** + String get selectOnePlayer; + + /// No description provided for @votar. + /// + /// In es, this message translates to: + /// **'Votar'** + String get votar; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index d7b2a38..e3b41c6 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -109,6 +109,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get impostorClueDescription => 'المنتحل يعرف الفئة'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ وقت النقاش'; @@ -477,6 +480,21 @@ class AppLocalizationsAr extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -531,4 +549,34 @@ class AppLocalizationsAr extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index cbd3459..af2f3cf 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -109,6 +109,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get impostorClueDescription => 'L\'impostor coneix la categoria'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Temps de debat'; @@ -480,6 +483,21 @@ class AppLocalizationsCa extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -534,4 +552,34 @@ class AppLocalizationsCa extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index b5c24d5..a573e60 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -110,6 +110,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get impostorClueDescription => 'Der Hochstapler kennt die Kategorie'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Diskussionszeit'; @@ -483,6 +486,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -537,4 +555,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2aaeee7..1df577b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -109,6 +109,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get impostorClueDescription => 'The impostor knows the category'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Discussion time'; @@ -478,6 +481,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get connectedPlayers => 'Connected players'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Waiting for players...'; @@ -531,4 +549,34 @@ class AppLocalizationsEn extends AppLocalizations { @override String get orScanQR => 'Not showing up? Scan the host\'s QR code'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 4474ccd..27bb952 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -109,6 +109,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get impostorClueDescription => 'El impostor conoce la categoría'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Tiempo de debate'; @@ -479,6 +482,21 @@ class AppLocalizationsEs extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -533,4 +551,34 @@ class AppLocalizationsEs extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index cf087cc..30ac7fe 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -110,6 +110,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get impostorClueDescription => 'Inpostoreak kategoria ezagutzen du'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Eztabaida-denbora'; @@ -482,6 +485,21 @@ class AppLocalizationsEu extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -536,4 +554,34 @@ class AppLocalizationsEu extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 49bf30a..950fbbf 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -109,6 +109,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get impostorClueDescription => 'L\'imposteur connaît la catégorie'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Temps de débat'; @@ -480,6 +483,21 @@ class AppLocalizationsFr extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -534,4 +552,34 @@ class AppLocalizationsFr extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 6e73ab8..7ad471c 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -109,6 +109,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get impostorClueDescription => 'धोखेबाज़ को श्रेणी पता होगी'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ बहस का समय'; @@ -479,6 +482,21 @@ class AppLocalizationsHi extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -533,4 +551,34 @@ class AppLocalizationsHi extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index a94844d..4665660 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -109,6 +109,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get impostorClueDescription => 'L\'impostore conosce la categoria'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Tempo di discussione'; @@ -480,6 +483,21 @@ class AppLocalizationsIt extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -534,4 +552,34 @@ class AppLocalizationsIt extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 45040c1..9f678a0 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -109,6 +109,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get impostorClueDescription => 'インポスターにカテゴリーが表示されます'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ 議論の時間'; @@ -477,6 +480,21 @@ class AppLocalizationsJa extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -531,4 +549,34 @@ class AppLocalizationsJa extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 47f58fc..5264563 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -109,6 +109,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get impostorClueDescription => '임포스터가 카테고리를 알 수 있습니다'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ 토론 시간'; @@ -477,6 +480,21 @@ class AppLocalizationsKo extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -531,4 +549,34 @@ class AppLocalizationsKo extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index e7f1c31..aca3d93 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -109,6 +109,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get impostorClueDescription => 'De bedrieger kent de categorie'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Debattijd'; @@ -480,6 +483,21 @@ class AppLocalizationsNl extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -534,4 +552,34 @@ class AppLocalizationsNl extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 1cf3627..01d2084 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -109,6 +109,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get impostorClueDescription => 'Oszust zna kategorię'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Czas debaty'; @@ -480,6 +483,21 @@ class AppLocalizationsPl extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -534,4 +552,34 @@ class AppLocalizationsPl extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 452f56f..23476ce 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -109,6 +109,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get impostorClueDescription => 'O impostor conhece a categoria'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Tempo de debate'; @@ -481,6 +484,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -535,4 +553,34 @@ class AppLocalizationsPt extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 854d706..8043790 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -109,6 +109,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get impostorClueDescription => 'Самозванец знает категорию'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Время обсуждения'; @@ -480,6 +483,21 @@ class AppLocalizationsRu extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -534,4 +552,34 @@ class AppLocalizationsRu extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 9444193..544ba41 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -109,6 +109,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get impostorClueDescription => 'Sahtekar kategoriyi bilir'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ Tartışma süresi'; @@ -479,6 +482,21 @@ class AppLocalizationsTr extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -533,4 +551,34 @@ class AppLocalizationsTr extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index b14294e..a2bf61c 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -109,6 +109,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get impostorClueDescription => '冒牌者可以知道分类'; + @override + String get debate => '🗣️ Debate'; + @override String get debateTime => '⏱️ 讨论时间'; @@ -476,6 +479,21 @@ class AppLocalizationsZh extends AppLocalizations { @override String get connectedPlayers => 'Jugadores conectados'; + @override + String get hostGame => 'Gestor de partida'; + + @override + String get waitingPlayersSeeWord => 'Esperando que todos vean su palabra...'; + + @override + String get activePlayers => 'Jugadores activos'; + + @override + String get playersVoted => 'Han votado'; + + @override + String get waitingVoting => 'Esperando que voten...'; + @override String get waitingForPlayers => 'Esperando jugadores...'; @@ -530,6 +548,36 @@ class AppLocalizationsZh extends AppLocalizations { @override String get orScanQR => '¿No aparece? Escanea el QR del host'; + + @override + String get iveSeenIt => 'Ya la he visto'; + + @override + String clueIs(String category) { + return 'La pista es: $category'; + } + + @override + String get debatePhaseActive => 'Fase de debate activa'; + + @override + String get debateInstructions => + 'Hablad entre vosotros y decid quién creéis que es el impostor. Cuando estéis listos, solicitad la votación.'; + + @override + String get solicitarVotacion => 'Solicitar votación'; + + @override + String get votacionSolicitada => 'Votación solicitada'; + + @override + String get whoDoYouThinkIsTheImpostor => '¿Quién es el impostor?'; + + @override + String get selectOnePlayer => 'Selecciona a un jugador para votar'; + + @override + String get votar => 'Votar'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/pantallas/pantalla_crear_partida.dart b/lib/pantallas/pantalla_crear_partida.dart index 4bf6516..fcf7c57 100644 --- a/lib/pantallas/pantalla_crear_partida.dart +++ b/lib/pantallas/pantalla_crear_partida.dart @@ -7,7 +7,9 @@ import '../modelos/partida.dart'; import '../servicios/servicio_nearby.dart'; import '../servicios/servicio_permisos.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 { @@ -30,23 +32,28 @@ class _PantallaCrearPartidaState extends State { int get _maxImpostores => (_jugadores.length / 3).floor().clamp(1, 4); - List _etiquetasTiempo(AppLocalizations l10n) => - [l10n.noLimit, l10n.oneMin, l10n.twoMin, l10n.threeMin, l10n.fiveMin]; + 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)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.playerAlreadyExists))); return; } if (_jugadores.length >= 20) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.maxPlayersReached)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.maxPlayersReached))); return; } setState(() { @@ -76,9 +83,9 @@ class _PantallaCrearPartidaState extends State { } if (_jugadores.length < 3) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.minPlayersRequired)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.minPlayersRequired))); return; } @@ -106,7 +113,9 @@ class _PantallaCrearPartidaState extends State { if (!permisosOk) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Se necesitan permisos de Bluetooth y ubicación')), + const SnackBar( + content: Text('Se necesitan permisos de Bluetooth y ubicación'), + ), ); } return; @@ -125,7 +134,9 @@ class _PantallaCrearPartidaState extends State { if (!ok) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No se pudo crear la sala. Verifica Bluetooth.')), + const SnackBar( + content: Text('No se pudo crear la sala. Verifica Bluetooth.'), + ), ); } return; @@ -163,7 +174,8 @@ class _PantallaCrearPartidaState extends State { final jugadorNearby = nearby.jugadores[i]; // El jugador [0] es el host, los de nearby son [1..n] final jugadorPartida = partida.jugadores[i + 1]; - impostores[jugadorNearby.endpointId] = jugadorPartida.esImpostor; + impostores[jugadorNearby.endpointId] = + jugadorPartida.esImpostor; } nearby.enviarInicioPartida( @@ -174,7 +186,19 @@ class _PantallaCrearPartidaState extends State { Navigator.pushReplacement( context, - MaterialPageRoute(builder: (_) => const PantallaVerPalabra()), + MaterialPageRoute( + builder: (_) => PantallaGestorHost( + onPartidaFin: () { + estado.limpiar(); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const PantallaPrincipal(), + ), + ); + }, + ), + ), ); }, ), @@ -241,8 +265,10 @@ class _PantallaCrearPartidaState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.gameMode, - style: Theme.of(context).textTheme.titleLarge), + Text( + l10n.gameMode, + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox(height: 12), SegmentedButton( segments: [ @@ -275,8 +301,10 @@ class _PantallaCrearPartidaState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.category, - style: Theme.of(context).textTheme.titleLarge), + Text( + l10n.category, + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox(height: 12), SizedBox( width: double.infinity, @@ -288,7 +316,9 @@ class _PantallaCrearPartidaState extends State { items: categorias.map((c) { return DropdownMenuItem( value: c, - child: Text(BancoPalabras.nombreBonitoCategoria(c, l10n)), + child: Text( + BancoPalabras.nombreBonitoCategoria(c, l10n), + ), ); }).toList(), onChanged: (v) => setState(() => _categoria = v!), @@ -310,10 +340,14 @@ class _PantallaCrearPartidaState extends State { 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), + Text( + l10n.playersCount(_jugadores.length), + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + l10n.playersRangeHint, + style: Theme.of(context).textTheme.bodyMedium, + ), ], ), const SizedBox(height: 12), @@ -342,13 +376,17 @@ class _PantallaCrearPartidaState extends State { return ListTile( leading: CircleAvatar( backgroundColor: TemaApp.colorTarjeta, - child: Text('${e.key + 1}', - style: - const TextStyle(color: TemaApp.colorTexto)), + child: Text( + '${e.key + 1}', + style: const TextStyle(color: TemaApp.colorTexto), + ), ), title: Text(e.value), trailing: IconButton( - icon: const Icon(Icons.close, color: TemaApp.colorAcento), + icon: const Icon( + Icons.close, + color: TemaApp.colorAcento, + ), onPressed: () => _eliminarJugador(e.key), ), dense: true, @@ -367,8 +405,10 @@ class _PantallaCrearPartidaState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.configuration, - style: Theme.of(context).textTheme.titleLarge), + Text( + l10n.configuration, + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox(height: 12), // Número de impostores @@ -384,10 +424,10 @@ class _PantallaCrearPartidaState extends State { : null, icon: const Icon(Icons.remove_circle_outline), ), - Text('$_numImpostores', - style: Theme.of(context) - .textTheme - .titleLarge), + Text( + '$_numImpostores', + style: Theme.of(context).textTheme.titleLarge, + ), IconButton( onPressed: _numImpostores < _maxImpostores ? () => setState(() => _numImpostores++) @@ -404,8 +444,7 @@ class _PantallaCrearPartidaState extends State { title: Text(l10n.impostorClue), subtitle: Text(l10n.impostorClueDescription), value: _pistaImpostor, - onChanged: (v) => - setState(() => _pistaImpostor = v), + onChanged: (v) => setState(() => _pistaImpostor = v), contentPadding: EdgeInsets.zero, ), @@ -423,8 +462,7 @@ class _PantallaCrearPartidaState extends State { child: Text(etiquetas[i]), ), ), - onChanged: (v) => - setState(() => _tiempoDebate = v), + onChanged: (v) => setState(() => _tiempoDebate = v), ), ], ), @@ -439,7 +477,9 @@ class _PantallaCrearPartidaState extends State { width: double.infinity, height: 56, child: ElevatedButton.icon( - onPressed: (_modoMultimovil || _jugadores.length >= 3) ? _iniciarPartida : null, + onPressed: (_modoMultimovil || _jugadores.length >= 3) + ? _iniciarPartida + : null, icon: const Icon(Icons.play_arrow), label: Text(l10n.startGame), style: ElevatedButton.styleFrom( diff --git a/lib/pantallas/pantalla_debate_cliente.dart b/lib/pantallas/pantalla_debate_cliente.dart new file mode 100644 index 0000000..b9c25b6 --- /dev/null +++ b/lib/pantallas/pantalla_debate_cliente.dart @@ -0,0 +1,156 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:farolero/l10n/generated/app_localizations.dart'; +import 'package:farolero/tema/tema_app.dart'; + +/// Pantalla que ve el jugador durante la fase de debate (multidispositivo). +/// El cliente recibe el cambio de fase via Nearby y se navega aquí. +class PantallaDebateCliente extends StatefulWidget { + final int? tiempoDebateSegundos; + final VoidCallback onSolicitarVotacion; + + const PantallaDebateCliente({ + super.key, + this.tiempoDebateSegundos, + required this.onSolicitarVotacion, + }); + + @override + State createState() => _PantallaDebateClienteState(); +} + +class _PantallaDebateClienteState extends State { + Timer? _timer; + int _segundosRestantes = 0; + bool _votacionSolicitada = false; + + @override + void initState() { + super.initState(); + if (widget.tiempoDebateSegundos != null) { + _segundosRestantes = widget.tiempoDebateSegundos!; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_segundosRestantes > 0) { + setState(() => _segundosRestantes--); + } else { + timer.cancel(); + } + }); + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + String _formatearTiempo(int segundos) { + final min = segundos ~/ 60; + final seg = segundos % 60; + return '${min.toString().padLeft(2, '0')}:${seg.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: TemaApp.colorFondo, + appBar: AppBar( + title: Text(l10n.debate), + automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + const Spacer(), + + // Timer + if (widget.tiempoDebateSegundos != null) ...[ + Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: _segundosRestantes == 0 + ? TemaApp.colorAcento.withValues(alpha: 0.3) + : TemaApp.colorTarjeta, + borderRadius: BorderRadius.circular(24), + ), + child: Column( + children: [ + Text( + _segundosRestantes == 0 + ? l10n.timeUp + : l10n.timeRemaining, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + _formatearTiempo(_segundosRestantes), + style: Theme.of(context).textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.bold, + color: _segundosRestantes == 0 + ? TemaApp.colorAcento + : TemaApp.colorTexto, + ), + ), + ], + ), + ), + const SizedBox(height: 32), + ] else ...[ + Text( + l10n.debatePhaseActive, + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ], + + // Instrucciones + Text( + l10n.debateInstructions, + textAlign: TextAlign.center, + style: TextStyle( + color: TemaApp.colorTextoSecundario, + fontSize: 16, + ), + ), + + const Spacer(), + + // Botón solicitar votación + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: _votacionSolicitada + ? null + : () { + setState(() => _votacionSolicitada = true); + widget.onSolicitarVotacion(); + }, + icon: Icon(_votacionSolicitada ? Icons.hourglass_empty : Icons.how_to_vote), + label: Text( + _votacionSolicitada + ? l10n.votacionSolicitada + : l10n.solicitarVotacion, + ), + style: ElevatedButton.styleFrom( + backgroundColor: _votacionSolicitada + ? TemaApp.colorTarjeta + : TemaApp.colorAcento, + foregroundColor: Colors.white, + textStyle: const TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pantallas/pantalla_gestor_host.dart b/lib/pantallas/pantalla_gestor_host.dart new file mode 100644 index 0000000..3783123 --- /dev/null +++ b/lib/pantallas/pantalla_gestor_host.dart @@ -0,0 +1,514 @@ +import 'dart:async'; +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/partida.dart'; +import '../servicios/servicio_nearby.dart'; +import '../tema/tema_app.dart'; + + +class PantallaGestorHost extends StatefulWidget { + final VoidCallback onPartidaFin; + + const PantallaGestorHost({super.key, required this.onPartidaFin}); + + @override + State createState() => _PantallaGestorHostState(); +} + +class _PantallaGestorHostState extends State { + Timer? _timer; + int _segundosRestantes = 0; + final Map _clientesListos = {}; + final Map _votosRecibidos = {}; + + @override + void initState() { + super.initState(); + _iniciarTemporizador(); + _registrarListeners(); + } + + void _iniciarTemporizador() { + final estado = context.read(); + final tiempo = estado.partida?.config.tiempoDebateSegundos; + if (tiempo != null) { + _segundosRestantes = tiempo; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_segundosRestantes > 0) { + setState(() => _segundosRestantes--); + } else { + timer.cancel(); + } + }); + } + } + + void _registrarListeners() { + final nearby = context.read(); + nearby.onMensaje((endpointId, mensaje) { + if (mensaje.tipo == TipoMensaje.listo) { + setState(() => _clientesListos[endpointId] = true); + } else if (mensaje.tipo == TipoMensaje.voto) { + final votanteId = mensaje.datos['votanteId'] as String?; + final votoId = mensaje.datos['votoporId'] as String?; + if (votanteId != null && votoId != null) { + setState(() => _votosRecibidos[votanteId] = votoId); + } + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + String _formatearTiempo(int segundos) { + final min = segundos ~/ 60; + final seg = segundos % 60; + return '${min.toString().padLeft(2, '0')}:${seg.toString().padLeft(2, '0')}'; + } + + void _avanzarAFase(FaseJuego fase) { + final estado = context.read(); + final nearby = context.read(); + + switch (fase) { + case FaseJuego.debate: + estado.iniciarDebate(); + nearby.enviarCambioFase('debate'); + _iniciarTemporizador(); + break; + case FaseJuego.votacion: + estado.iniciarVotacion(); + nearby.enviarCambioFase('votacion'); + _votosRecibidos.clear(); + break; + case FaseJuego.resultado: + final resultado = estado.procesarVotacion(); + if (resultado != null) { + nearby.enviarResultadoVotacion({ + 'eliminadoId': resultado.eliminadoId, + 'eliminadoNombre': resultado.eliminadoNombre, + 'eraImpostor': resultado.eraImpostor, + 'votos': resultado.votos, + }); + } + break; + default: + break; + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final estado = context.watch(); + final nearby = context.watch(); + final partida = estado.partida; + + if (partida == null) { + return Scaffold( + appBar: AppBar(title: Text(l10n.hostGame)), + body: const Center(child: Text('Error: Sin partida')), + ); + } + + final numJugadores = partida.jugadores.length + 1; + final todosListos = _clientesListos.length >= numJugadores - 1; + final todosVotaron = _votosRecibidos.length >= numJugadores - 1; + + return Scaffold( + appBar: AppBar( + title: Text(l10n.hostGame), + automaticallyImplyLeading: false, + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await nearby.desconectar(); + widget.onPartidaFin(); + }, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildFaseIndicator(context, partida.fase, l10n), + const SizedBox(height: 16), + Expanded( + child: _buildContenidoFase( + context, + partida.fase, + l10n, + todosListos, + todosVotaron, + ), + ), + const SizedBox(height: 16), + _buildBotonAccion( + context, + partida.fase, + l10n, + todosListos, + todosVotaron, + ), + ], + ), + ), + ); + } + + Widget _buildFaseIndicator( + BuildContext context, + FaseJuego fase, + AppLocalizations l10n, + ) { + final fases = [ + (FaseJuego.verPalabra, l10n.seeYourWord), + (FaseJuego.debate, l10n.debate), + (FaseJuego.votacion, l10n.voting), + (FaseJuego.resultado, l10n.result), + ]; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: fases.map((e) { + final esActiva = fase == e.$1 || fase.index > e.$1.index; + return Container( + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: esActiva ? TemaApp.colorAcento : TemaApp.colorSuperficie, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + e.$2, + style: TextStyle( + color: esActiva ? Colors.white : TemaApp.colorTextoSecundario, + fontWeight: esActiva ? FontWeight.bold : FontWeight.normal, + ), + ), + ); + }).toList(), + ), + ); + } + + Widget _buildContenidoFase( + BuildContext context, + FaseJuego fase, + AppLocalizations l10n, + bool todosListos, + bool todosVotaron, + ) { + final nearby = context.watch(); + + switch (fase) { + case FaseJuego.verPalabra: + return _buildFaseVerPalabra(context, l10n, todosListos, nearby); + case FaseJuego.debate: + return _buildFaseDebate(context, l10n, nearby); + case FaseJuego.votacion: + return _buildFaseVotacion(context, l10n, todosVotaron, nearby); + default: + return const Center(child: Text('Fin de la partida')); + } + } + + Widget _buildFaseVerPalabra( + BuildContext context, + AppLocalizations l10n, + bool todosListos, + ServicioNearby nearby, + ) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.waitingPlayersSeeWord, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Text( + l10n.connectedPlayers, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + _buildJugadorTile(nearby.miNombre ?? 'Host', true, false), + ...nearby.jugadores.map( + (j) => _buildJugadorTile( + j.nombre, + false, + _clientesListos[j.endpointId] ?? false, + ), + ), + const Spacer(), + if (todosListos) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: TemaApp.colorVerde.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle, color: TemaApp.colorVerde), + const SizedBox(width: 8), + Text( + l10n.allSeenStartDebate, + style: const TextStyle(color: TemaApp.colorVerde), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildFaseDebate( + BuildContext context, + AppLocalizations l10n, + ServicioNearby nearby, + ) { + final estado = context.read(); + final tiempo = estado.partida?.config.tiempoDebateSegundos; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (tiempo != null) ...[ + Text(l10n.debate, style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _segundosRestantes == 0 + ? TemaApp.colorAcento.withValues(alpha: 0.3) + : TemaApp.colorTarjeta, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + _segundosRestantes == 0 + ? l10n.timeUp + : l10n.timeRemaining, + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + _formatearTiempo(_segundosRestantes), + style: Theme.of(context).textTheme.headlineLarge, + ), + ], + ), + ), + const SizedBox(height: 16), + ], + Text( + l10n.activePlayers, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemCount: nearby.jugadores.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return _buildJugadorTile( + nearby.miNombre ?? 'Host', + true, + true, + ); + } + final j = nearby.jugadores[index - 1]; + return _buildJugadorTile(j.nombre, false, true); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildFaseVotacion( + BuildContext context, + AppLocalizations l10n, + bool todosVotaron, + ServicioNearby nearby, + ) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.voting, style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: TemaApp.colorTarjeta, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + l10n.votesProgress( + _votosRecibidos.length, + nearby.jugadores.length + 1, + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: + _votosRecibidos.length / + (nearby.jugadores.length + 1), + backgroundColor: TemaApp.colorSuperficie, + valueColor: const AlwaysStoppedAnimation( + TemaApp.colorAcento, + ), + minHeight: 8, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Text( + l10n.playersVoted, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemCount: nearby.jugadores.length + 1, + itemBuilder: (context, index) { + final esHost = index == 0; + final nombre = esHost + ? (nearby.miNombre ?? 'Host') + : nearby.jugadores[index - 1].nombre; + final haVotado = + esHost || _votosRecibidos.containsKey(nombre); + return _buildJugadorTile(nombre, esHost, haVotado); + }, + ), + ), + if (todosVotaron) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: TemaApp.colorVerde.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle, color: TemaApp.colorVerde), + const SizedBox(width: 8), + Text( + l10n.allVoted, + style: const TextStyle(color: TemaApp.colorVerde), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildJugadorTile(String nombre, bool esHost, bool listo) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: listo + ? TemaApp.colorVerde.withValues(alpha: 0.2) + : TemaApp.colorTarjeta, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Text(esHost ? '👑' : '🎭', style: const TextStyle(fontSize: 18)), + const SizedBox(width: 8), + Expanded(child: Text(nombre)), + if (listo) + const Icon(Icons.check_circle, color: TemaApp.colorVerde, size: 20), + ], + ), + ); + } + + Widget _buildBotonAccion( + BuildContext context, + FaseJuego fase, + AppLocalizations l10n, + bool todosListos, + bool todosVotaron, + ) { + switch (fase) { + case FaseJuego.verPalabra: + return SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: todosListos + ? () => _avanzarAFase(FaseJuego.debate) + : null, + icon: const Icon(Icons.forum), + label: Text( + todosListos + ? l10n.allSeenStartDebate + : l10n.waitingPlayersSeeWord, + ), + ), + ); + case FaseJuego.debate: + return SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () => _avanzarAFase(FaseJuego.votacion), + icon: const Icon(Icons.how_to_vote), + label: Text(l10n.goToVoting), + ), + ); + case FaseJuego.votacion: + return SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: todosVotaron + ? () => _avanzarAFase(FaseJuego.resultado) + : null, + icon: const Icon(Icons.visibility), + label: Text(todosVotaron ? l10n.revealResult : l10n.waitingVoting), + ), + ); + default: + return const SizedBox.shrink(); + } + } +} diff --git a/lib/pantallas/pantalla_palabra_cliente.dart b/lib/pantallas/pantalla_palabra_cliente.dart new file mode 100644 index 0000000..e37df06 --- /dev/null +++ b/lib/pantallas/pantalla_palabra_cliente.dart @@ -0,0 +1,169 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:farolero/l10n/generated/app_localizations.dart'; +import 'package:farolero/tema/tema_app.dart'; + +/// Pantalla que ve cada jugador cuando recibe su palabra (modo multidispositivo). +/// El cliente recibe la palabra via ServicioNearby y se navega aquí. +/// NO es la pantalla del host. +class PantallaPalabraCliente extends StatefulWidget { + final String palabra; + final bool esImpostor; + final String? pistaCategoria; + final VoidCallback onVisto; + + const PantallaPalabraCliente({ + super.key, + required this.palabra, + required this.esImpostor, + this.pistaCategoria, + required this.onVisto, + }); + + @override + State createState() => _PantallaPalabraClienteState(); +} + +class _PantallaPalabraClienteState extends State { + bool _palabraVisible = false; + Timer? _timer; + + void _togglePalabra() { + setState(() => _palabraVisible = !_palabraVisible); + _timer?.cancel(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: TemaApp.colorFondo, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + const Spacer(), + // Tarjeta de palabra + GestureDetector( + onTap: _togglePalabra, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 24), + decoration: BoxDecoration( + color: _palabraVisible + ? TemaApp.colorAcento + : TemaApp.colorTarjeta, + borderRadius: BorderRadius.circular(24), + boxShadow: _palabraVisible + ? [ + BoxShadow( + color: TemaApp.colorAcento.withValues(alpha: 0.4), + blurRadius: 24, + spreadRadius: 2, + ), + ] + : null, + ), + child: Column( + children: [ + Icon( + _palabraVisible ? Icons.visibility : Icons.visibility_off, + color: _palabraVisible + ? Colors.white + : TemaApp.colorTextoSecundario, + size: 32, + ), + const SizedBox(height: 16), + Text( + _palabraVisible ? widget.palabra : '???', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: _palabraVisible + ? Colors.white + : TemaApp.colorTextoSecundario, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Pista para impostores + if (widget.esImpostor && widget.pistaCategoria != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: TemaApp.colorAcento.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.lightbulb, color: TemaApp.colorAcento), + const SizedBox(width: 8), + Flexible( + child: Text( + '🎭 ${l10n.clueIs(widget.pistaCategoria!)}', + style: const TextStyle(color: TemaApp.colorAcento), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + ], + + // Instrucciones + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + _palabraVisible + ? 'Mantén la pantalla oculta. No la enseñes a nadie.' + : 'Toca para ver tu palabra', + textAlign: TextAlign.center, + style: TextStyle( + color: TemaApp.colorTextoSecundario, + fontSize: 14, + ), + ), + ), + + const Spacer(), + + // Botón confirmar + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () { + widget.onVisto(); + }, + icon: const Icon(Icons.check), + label: Text(l10n.iveSeenIt), + style: ElevatedButton.styleFrom( + backgroundColor: TemaApp.colorAcento, + foregroundColor: Colors.white, + textStyle: const TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pantallas/pantalla_unirse.dart b/lib/pantallas/pantalla_unirse.dart index 81af3cd..a41605d 100644 --- a/lib/pantallas/pantalla_unirse.dart +++ b/lib/pantallas/pantalla_unirse.dart @@ -2,9 +2,13 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:provider/provider.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; +import '../modelos/jugador.dart'; import '../servicios/servicio_nearby.dart'; import '../servicios/servicio_permisos.dart'; import '../tema/tema_app.dart'; +import 'pantalla_palabra_cliente.dart'; +import 'pantalla_debate_cliente.dart'; +import 'pantalla_votacion_cliente.dart'; /// Pantalla para unirse a una partida multidispositivo. /// Flujo: nombre → discovery automático (lista de salas) → fallback QR @@ -26,6 +30,110 @@ class _PantallaUnirseState extends State { String? _error; String? _salaSeleccionada; + // Estado del juego recibido del host + String? _palabraRecibida; + bool _esImpostor = false; + String? _pistaCategoria; + final List _jugadores = []; + + @override + void initState() { + super.initState(); + // Registrar listener ANTES del primer build + WidgetsBinding.instance.addPostFrameCallback((_) { + _registrarListenerPartida(); + }); + } + + void _registrarListenerPartida() { + final nearby = context.read(); + nearby.onMensaje((endpointId, mensaje) { + if (mensaje.tipo == TipoMensaje.partidaInicio) { + // El host ha iniciado la partida — nos ha enviado nuestra palabra + setState(() { + _palabraRecibida = mensaje.datos['palabra'] as String?; + _esImpostor = mensaje.datos['esImpostor'] as bool? ?? false; + _pistaCategoria = mensaje.datos['categoria'] as String?; + }); + // Navegar a pantalla de palabra del cliente + if (mounted && _palabraRecibida != null) { + _navegarAPalabra(); + } + } else if (mensaje.tipo == TipoMensaje.fase) { + // El host cambia de fase — navegar a la pantalla correspondiente + final fase = mensaje.datos['fase'] as String?; + if (mounted && fase != null) { + _navegarSegunFase(fase); + } + } + }); + } + + void _navegarAPalabra() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => PantallaPalabraCliente( + palabra: _palabraRecibida ?? '', + esImpostor: _esImpostor, + pistaCategoria: _pistaCategoria, + onVisto: () { + // Enviar "listo" al host y volver a la espera + final nearby = context.read(); + if (nearby.hostEndpointId != null) { + nearby.enviarMensaje( + nearby.hostEndpointId!, + MensajeP2P(tipo: TipoMensaje.listo, datos: {}), + ); + } + Navigator.of(context).pop(); + }, + ), + ), + ); + } + + void _navegarSegunFase(String fase) { + switch (fase) { + case 'debate': + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => PantallaDebateCliente( + tiempoDebateSegundos: null, + onSolicitarVotacion: () { + final nearby = context.read(); + if (nearby.hostEndpointId != null) { + nearby.enviarMensaje( + nearby.hostEndpointId!, + MensajeP2P(tipo: TipoMensaje.ping, datos: {'solicitoVotacion': true}), + ); + } + }, + ), + ), + ); + break; + case 'votacion': + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => PantallaVotacionCliente( + jugadores: _jugadores, + onVoto: (votoporId) { + final nearby = context.read(); + if (nearby.hostEndpointId != null) { + nearby.enviarMensaje( + nearby.hostEndpointId!, + MensajeP2P(tipo: TipoMensaje.voto, datos: {'votoporId': votoporId}), + ); + } + Navigator.of(context).pop(); + }, + ), + ), + ); + break; + } + } + @override void dispose() { _nombreController.dispose(); diff --git a/lib/pantallas/pantalla_votacion_cliente.dart b/lib/pantallas/pantalla_votacion_cliente.dart new file mode 100644 index 0000000..c541425 --- /dev/null +++ b/lib/pantallas/pantalla_votacion_cliente.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:farolero/l10n/generated/app_localizations.dart'; +import 'package:farolero/modelos/jugador.dart'; +import 'package:farolero/tema/tema_app.dart'; + +/// Pantalla de votación para el cliente (multidispositivo). +/// El cliente recibe fase=votacion y ve esta pantalla para elegir a quién votar. +class PantallaVotacionCliente extends StatefulWidget { + final List jugadores; + final Function(String votoporId) onVoto; + + const PantallaVotacionCliente({ + super.key, + required this.jugadores, + required this.onVoto, + }); + + @override + State createState() => _PantallaVotacionClienteState(); +} + +class _PantallaVotacionClienteState extends State { + String? _votoSeleccionado; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: TemaApp.colorFondo, + appBar: AppBar( + title: Text(l10n.voting), + automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.whoDoYouThinkIsTheImpostor, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + l10n.selectOnePlayer, + style: TextStyle(color: TemaApp.colorTextoSecundario), + ), + const SizedBox(height: 16), + Expanded( + child: ListView.builder( + itemCount: widget.jugadores.length, + itemBuilder: (context, index) { + final jugador = widget.jugadores[index]; + final selected = _votoSeleccionado == jugador.id; + return Card( + color: selected + ? TemaApp.colorAcento.withValues(alpha: 0.3) + : TemaApp.colorTarjeta, + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: selected + ? TemaApp.colorAcento + : TemaApp.colorAcento.withValues(alpha: 0.3), + child: Text( + '${index + 1}', + style: TextStyle( + color: selected + ? Colors.white + : TemaApp.colorTexto, + ), + ), + ), + title: Text(jugador.nombre), + trailing: selected + ? const Icon(Icons.check_circle, + color: TemaApp.colorAcento) + : null, + onTap: () { + setState(() => _votoSeleccionado = jugador.id); + }, + ), + ); + }, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: _votoSeleccionado == null + ? null + : () => widget.onVoto(_votoSeleccionado!), + icon: const Icon(Icons.how_to_vote), + label: Text(l10n.votar), + style: ElevatedButton.styleFrom( + backgroundColor: TemaApp.colorAcento, + foregroundColor: Colors.white, + textStyle: const TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } +}