import 'dart:convert'; import 'dart:math'; import 'package:flutter/services.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; /// Una palabra del banco junto con la pista que verá el impostor. class EntradaPalabra { final String palabra; /// Pista específica de esta palabra. Si es null se usa la de la categoría. final String? pista; const EntradaPalabra({required this.palabra, this.pista}); } /// Categorías disponibles en el banco de palabras. class BancoPalabras { final Map> categorias; final Map pistasPorCategoria; /// Pista por palabra, cuando el banco la aporta. final Map pistasPorPalabra; BancoPalabras( this.categorias, { Map? pistasPorCategoria, Map? pistasPorPalabra, }) : pistasPorCategoria = pistasPorCategoria ?? {}, pistasPorPalabra = pistasPorPalabra ?? {}; static final Map _instancias = {}; static Future cargar({String idioma = 'es'}) async { if (_instancias.containsKey(idioma)) return _instancias[idioma]!; String jsonStr; try { jsonStr = await rootBundle.loadString( 'assets/words/palabras_$idioma.json', ); } catch (_) { try { final archivoLegacy = idioma == 'es' ? 'assets/palabras.json' : 'assets/palabras_$idioma.json'; jsonStr = await rootBundle.loadString(archivoLegacy); } catch (_) { if (idioma != 'es') return cargar(idioma: 'es'); rethrow; } } final data = json.decode(jsonStr) as Map; final cats = data['categorias'] as Map; final mapa = >{}; final pistas = {}; final pistasPalabra = {}; for (final entrada in cats.entries) { final valor = entrada.value; final listaCruda = valor is Map ? valor['palabras'] as List : valor as List; if (valor is Map) { final pista = valor['pista']; if (pista is String && pista.isNotEmpty) pistas[entrada.key] = pista; } final palabras = []; for (final elemento in listaCruda) { // Formato v2: "Perro". Formato v3: {"palabra": "Perro", "pista": "..."} if (elemento is Map) { final palabra = elemento['palabra'] as String?; if (palabra == null || palabra.isEmpty) continue; palabras.add(palabra); final pistaPalabra = elemento['pista']; if (pistaPalabra is String && pistaPalabra.isNotEmpty) { pistasPalabra[palabra] = pistaPalabra; } } else { palabras.add(elemento as String); } } mapa[entrada.key] = palabras; } _instancias[idioma] = BancoPalabras( mapa, pistasPorCategoria: pistas, pistasPorPalabra: pistasPalabra, ); return _instancias[idioma]!; } List get nombresCategorias => categorias.keys.toList(); /// Obtiene una palabra aleatoria de la categoría dada (o de todas si es null). String palabraAleatoria(String? categoria) { final rng = Random.secure(); if (categoria == null || categoria == 'todas') { final todasPalabras = categorias.values.expand((l) => l).toList(); return todasPalabras[rng.nextInt(todasPalabras.length)]; } final lista = categorias[categoria]!; return lista[rng.nextInt(lista.length)]; } /// Devuelve la categoría a la que pertenece una palabra. String? categoriaDepalabra(String palabra) { for (final entrada in categorias.entries) { if (entrada.value.contains(palabra)) return entrada.key; } return null; } /// Devuelve la pista localizada de una categoría si el banco la trae. String? pistaDeCategoria(String categoria) => pistasPorCategoria[categoria]; /// Pista que verá el impostor para una palabra concreta. Prioriza la pista /// específica de la palabra y cae a la de su categoría si no existe. String? pistaDePalabra(String palabra, {String? categoria}) { final especifica = pistasPorPalabra[palabra]; if (especifica != null && especifica.isNotEmpty) return especifica; final clave = categoria ?? categoriaDepalabra(palabra); if (clave == null) return null; return pistasPorCategoria[clave]; } /// Devuelve el nombre localizado de la categoría usando AppLocalizations. static String nombreBonitoCategoria(String clave, [AppLocalizations? l10n]) { if (l10n != null) { final nombres = { 'todas': l10n.categoryAll, 'animales': l10n.categoryAnimals, 'comida': l10n.categoryFood, 'paises': l10n.categoryCountries, 'deportes': l10n.categorySports, 'profesiones': l10n.categoryProfessions, 'objetos': l10n.categoryObjects, 'lugares': l10n.categoryPlaces, 'peliculas': l10n.categoryMovies, 'musica': l10n.categoryMusic, 'tecnologia': l10n.categoryTechnology, }; return nombres[clave] ?? clave; } const nombres = { 'todas': 'Todas', 'animales': 'Animales', 'comida': 'Comida', 'paises': 'Países', 'deportes': 'Deportes', 'profesiones': 'Profesiones', 'objetos': 'Objetos', 'lugares': 'Lugares', 'peliculas': 'Películas', 'musica': 'Música', 'tecnologia': 'Tecnología', }; return nombres[clave] ?? clave; } } class EntradaPalabraTraducida { final String palabra; final String pista; const EntradaPalabraTraducida({required this.palabra, required this.pista}); } class BancoPalabrasTraducidas { final Map> categorias; const BancoPalabrasTraducidas(this.categorias); static final Map _instancias = {}; static Future cargar({String idioma = 'es'}) async { if (_instancias.containsKey(idioma)) return _instancias[idioma]!; final banco = await BancoPalabras.cargar(idioma: idioma); final mapa = >{}; for (final categoria in banco.categorias.entries) { final pistaImpostor = banco.pistaDeCategoria(categoria.key) ?? categoria.key; mapa[categoria.key] = categoria.value .map( (palabra) => EntradaPalabraTraducida( palabra: palabra, pista: banco.pistaDePalabra(palabra, categoria: categoria.key) ?? pistaImpostor, ), ) .toList(); } _instancias[idioma] = BancoPalabrasTraducidas(mapa); return _instancias[idioma]!; } }