diff --git a/assets/ui/generated/gameplay/gameplay_atmosphere_bg.png b/assets/ui/generated/gameplay/gameplay_atmosphere_bg.png new file mode 100644 index 0000000..b6005c4 Binary files /dev/null and b/assets/ui/generated/gameplay/gameplay_atmosphere_bg.png differ diff --git a/assets/ui/generated/gameplay/gameplay_phase_emblem.png b/assets/ui/generated/gameplay/gameplay_phase_emblem.png new file mode 100644 index 0000000..faa9f44 Binary files /dev/null and b/assets/ui/generated/gameplay/gameplay_phase_emblem.png differ diff --git a/assets/ui/generated/gameplay/notes_strategy_art.png b/assets/ui/generated/gameplay/notes_strategy_art.png new file mode 100644 index 0000000..29f444e Binary files /dev/null and b/assets/ui/generated/gameplay/notes_strategy_art.png differ diff --git a/lib/pantallas/pantalla_adivinanza.dart b/lib/pantallas/pantalla_adivinanza.dart index 951679d..3eb9bf7 100644 --- a/lib/pantallas/pantalla_adivinanza.dart +++ b/lib/pantallas/pantalla_adivinanza.dart @@ -50,6 +50,8 @@ class _PantallaAdivinanzaState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + const ArteGameplayFarolero.fase(height: 132), + const SizedBox(height: 12), EncabezadoFarolero( icono: Icons.theater_comedy, titulo: l10n.impostorCanGuess, diff --git a/lib/pantallas/pantalla_debate.dart b/lib/pantallas/pantalla_debate.dart index 5083715..85d5439 100644 --- a/lib/pantallas/pantalla_debate.dart +++ b/lib/pantallas/pantalla_debate.dart @@ -82,6 +82,8 @@ class _PantallaDebateState extends State { padding: const EdgeInsets.all(16), child: Column( children: [ + const ArteGameplayFarolero.fase(height: 110), + const SizedBox(height: 10), // Temporizador if (tieneTemporizador) ...[ Container( diff --git a/lib/pantallas/pantalla_debate_cliente.dart b/lib/pantallas/pantalla_debate_cliente.dart index 33a4ae3..853e0ee 100644 --- a/lib/pantallas/pantalla_debate_cliente.dart +++ b/lib/pantallas/pantalla_debate_cliente.dart @@ -164,6 +164,8 @@ class _PantallaDebateClienteState extends State { padding: const EdgeInsets.all(24), child: Column( children: [ + const ArteGameplayFarolero.fase(height: 124), + const SizedBox(height: 10), const Spacer(), // Timer @@ -242,7 +244,7 @@ class _PantallaDebateClienteState extends State { const SizedBox(width: 12), Expanded( child: Text( - 'Empieza ${widget.primerTurnoNombre} diciendo su palabra.', + widget.primerTurnoNombre!, style: Theme.of(context).textTheme.titleMedium, ), ), diff --git a/lib/pantallas/pantalla_gestor_host.dart b/lib/pantallas/pantalla_gestor_host.dart index 37b2639..f254bcc 100644 --- a/lib/pantallas/pantalla_gestor_host.dart +++ b/lib/pantallas/pantalla_gestor_host.dart @@ -209,28 +209,30 @@ class _PantallaGestorHostState extends State { child: Padding( padding: const EdgeInsets.all(16), child: Column( - children: [ - _buildAvisoClientesDesconectados(context, nearby), - _buildFaseIndicator(context, partida.fase, l10n), - const SizedBox(height: 16), - Expanded( - child: _buildContenidoFase( + children: [ + _buildAvisoClientesDesconectados(context, nearby), + _buildFaseIndicator(context, partida.fase, l10n), + const SizedBox(height: 8), + const ArteGameplayFarolero.fase(height: 92), + const SizedBox(height: 16), + Expanded( + child: _buildContenidoFase( + context, + partida.fase, + l10n, + todosListos, + todosVotaron, + ), + ), + const SizedBox(height: 16), + _buildBotonAccion( context, partida.fase, l10n, todosListos, todosVotaron, ), - ), - const SizedBox(height: 16), - _buildBotonAccion( - context, - partida.fase, - l10n, - todosListos, - todosVotaron, - ), - ], + ], ), ), ), @@ -703,8 +705,8 @@ class _PantallaGestorHostState extends State { icon: const Icon(Icons.how_to_vote), label: Text( _hostYaVoto(context) - ? 'Votos del host registrados' - : 'Votar por los jugadores de este móvil', + ? l10n.playersVoted + : l10n.confirmVote, ), ), ), @@ -1055,9 +1057,10 @@ class _PantallaGestorHostState extends State { ), child: Row( children: [ - Text( - esHost ? 'Host' : 'Cliente', - style: const TextStyle(fontSize: 18), + Icon( + esHost ? Icons.phone_android : Icons.devices, + color: esHost ? TemaApp.colorDorado : TemaApp.colorNaranja, + size: 22, ), const SizedBox(width: 8), Expanded(child: Text(nombre)), @@ -1410,9 +1413,14 @@ class _PantallaRevelarPalabraHostState child: _manteniendo ? Column( children: [ - Text( - widget.esImpostor ? 'Impostor' : 'Ciudadano', - style: const TextStyle(fontSize: 48), + Icon( + widget.esImpostor + ? Icons.theater_comedy + : Icons.key, + color: widget.esImpostor + ? TemaApp.colorAcento + : TemaApp.colorVerde, + size: 48, ), const SizedBox(height: 16), Text( diff --git a/lib/pantallas/pantalla_notas.dart b/lib/pantallas/pantalla_notas.dart index 0708761..5c618fd 100644 --- a/lib/pantallas/pantalla_notas.dart +++ b/lib/pantallas/pantalla_notas.dart @@ -104,6 +104,8 @@ class _PantallaNotasState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ArteGameplayFarolero.notas(height: 138), + const SizedBox(height: 10), Text( l10n.whoAreYou, style: Theme.of(context).textTheme.headlineMedium, @@ -159,6 +161,8 @@ class _PantallaNotasState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ArteGameplayFarolero.notas(height: 120), + const SizedBox(height: 8), Row( children: [ IconButton( diff --git a/lib/pantallas/pantalla_notas_online.dart b/lib/pantallas/pantalla_notas_online.dart index 37b0253..21b2517 100644 --- a/lib/pantallas/pantalla_notas_online.dart +++ b/lib/pantallas/pantalla_notas_online.dart @@ -130,6 +130,8 @@ class _PantallaNotasOnlineState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ArteGameplayFarolero.notas(height: 138), + const SizedBox(height: 10), Text( AppLocalizations.of(context)!.whoAreYou, style: Theme.of(context).textTheme.headlineSmall, @@ -165,6 +167,8 @@ class _PantallaNotasOnlineState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ArteGameplayFarolero.notas(height: 120), + const SizedBox(height: 8), Row( children: [ if (widget.autoresControlados.length > 1) diff --git a/lib/pantallas/pantalla_palabra_cliente.dart b/lib/pantallas/pantalla_palabra_cliente.dart index ca4ae72..06eba7a 100644 --- a/lib/pantallas/pantalla_palabra_cliente.dart +++ b/lib/pantallas/pantalla_palabra_cliente.dart @@ -56,6 +56,8 @@ class _PantallaPalabraClienteState extends State { padding: const EdgeInsets.all(24), child: Column( children: [ + const ArteGameplayFarolero.fase(height: 124), + const SizedBox(height: 12), const Spacer(), // Tarjeta de palabra GestureDetector( @@ -136,7 +138,7 @@ class _PantallaPalabraClienteState extends State { const SizedBox(width: 8), Flexible( child: Text( - '🎭 ${l10n.clueIs(widget.pistaCategoria!)}', + '\u{1F3AD} ${l10n.clueIs(widget.pistaCategoria!)}', style: const TextStyle(color: TemaApp.colorAcento), ), ), @@ -151,7 +153,7 @@ class _PantallaPalabraClienteState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _palabraVisible - ? 'Mantén la pantalla oculta. No la enseñes a nadie.' + ? l10n.makeSureNoOneLooks : _haRevelado ? l10n.seeYourWord : l10n.tapToSee, diff --git a/lib/pantallas/pantalla_palabras_cliente.dart b/lib/pantallas/pantalla_palabras_cliente.dart index 105f26f..8fc937c 100644 --- a/lib/pantallas/pantalla_palabras_cliente.dart +++ b/lib/pantallas/pantalla_palabras_cliente.dart @@ -55,8 +55,10 @@ class _PantallaPalabrasClienteState extends State { padding: const EdgeInsets.all(24), child: Column( children: [ + const ArteGameplayFarolero.fase(height: 116), + const SizedBox(height: 8), Text( - 'Jugador ${_indice + 1} de ${widget.jugadores.length}', + '${l10n.defaultPlayerName} ${_indice + 1} / ${widget.jugadores.length}', style: Theme.of(context).textTheme.titleMedium, ), const Spacer(), diff --git a/lib/pantallas/pantalla_reglas.dart b/lib/pantallas/pantalla_reglas.dart index d31f9a8..415ca1f 100644 --- a/lib/pantallas/pantalla_reglas.dart +++ b/lib/pantallas/pantalla_reglas.dart @@ -24,6 +24,8 @@ class PantallaReglas extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const ArteGameplayFarolero.notas(height: 126), + const SizedBox(height: 10), EncabezadoFarolero( icono: Icons.menu_book_rounded, titulo: l10n.rulesTitle, diff --git a/lib/pantallas/pantalla_ver_palabra.dart b/lib/pantallas/pantalla_ver_palabra.dart index e2eb6d2..da53cd2 100644 --- a/lib/pantallas/pantalla_ver_palabra.dart +++ b/lib/pantallas/pantalla_ver_palabra.dart @@ -38,6 +38,8 @@ class _PantallaVerPalabraState extends State { padding: const EdgeInsets.all(16), child: Column( children: [ + const ArteGameplayFarolero.fase(height: 110), + const SizedBox(height: 10), EncabezadoFarolero( icono: Icons.visibility, titulo: l10n.roundNumber(partida.rondaActual), @@ -174,6 +176,8 @@ class _PantallaRevelarPalabraState extends State<_PantallaRevelarPalabra> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + const ArteGameplayFarolero.fase(height: 128), + const SizedBox(height: 12), Text( widget.nombre, style: Theme.of(context).textTheme.headlineMedium, diff --git a/lib/pantallas/pantalla_votacion.dart b/lib/pantallas/pantalla_votacion.dart index 2ddd555..b789d48 100644 --- a/lib/pantallas/pantalla_votacion.dart +++ b/lib/pantallas/pantalla_votacion.dart @@ -65,6 +65,8 @@ class _PantallaVotacionState extends State { padding: const EdgeInsets.all(16), child: Column( children: [ + const ArteGameplayFarolero.fase(height: 108), + const SizedBox(height: 10), // Progreso de votos Container( width: double.infinity, @@ -192,41 +194,41 @@ class _PantallaVotacionState extends State { child: Padding( padding: const EdgeInsets.all(32), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('🗳️', style: TextStyle(fontSize: 64)), - const SizedBox(height: 24), - Text( - l10n.allVoted, - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 16), - Text( - l10n.tapToReveal, - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton.icon( - onPressed: () { - final resultado = estado.procesarVotacion(); - if (resultado != null) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (_) => - PantallaResultado(resultado: resultado), - ), - ); - } - }, - icon: const Icon(Icons.visibility), - label: Text(l10n.revealResult), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const ArteGameplayFarolero.fase(height: 132), + const SizedBox(height: 24), + Text( + l10n.allVoted, + style: Theme.of(context).textTheme.headlineMedium, ), - ), - ], + const SizedBox(height: 16), + Text( + l10n.tapToReveal, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () { + final resultado = estado.procesarVotacion(); + if (resultado != null) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => + PantallaResultado(resultado: resultado), + ), + ); + } + }, + icon: const Icon(Icons.visibility), + label: Text(l10n.revealResult), + ), + ), + ], ), ), ), diff --git a/lib/pantallas/pantalla_votacion_cliente.dart b/lib/pantallas/pantalla_votacion_cliente.dart index 40cd101..b6e7ba7 100644 --- a/lib/pantallas/pantalla_votacion_cliente.dart +++ b/lib/pantallas/pantalla_votacion_cliente.dart @@ -134,12 +134,12 @@ class _PantallaVotacionClienteState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ArteGameplayFarolero.fase(height: 108), + const SizedBox(height: 10), EncabezadoFarolero( icono: Icons.how_to_vote, titulo: l10n.whoDoYouThinkIsTheImpostor, - subtitulo: _modoMultiVotante - ? 'Emití un voto por cada jugador que manejás.' - : l10n.selectOnePlayer, + subtitulo: l10n.selectOnePlayer, color: TemaApp.colorAcento, trailing: Image.asset( 'assets/ui/premium/vote_danger_glow.png', @@ -254,6 +254,8 @@ class _PantallaVotacionClienteState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ArteGameplayFarolero.fase(height: 118), + const SizedBox(height: 10), Container( width: double.infinity, padding: const EdgeInsets.all(20), @@ -275,7 +277,7 @@ class _PantallaVotacionClienteState extends State { ), const SizedBox(height: 8), Text( - eraImpostor ? 'Era impostor' : 'Era inocente', + eraImpostor ? l10n.wasImpostor : l10n.wasInnocent, style: TextStyle( color: eraImpostor ? TemaApp.colorVerde diff --git a/lib/tema/componentes_farolero.dart b/lib/tema/componentes_farolero.dart index 18adcaf..99fec1c 100644 --- a/lib/tema/componentes_farolero.dart +++ b/lib/tema/componentes_farolero.dart @@ -22,6 +22,19 @@ class FondoFarolero extends StatelessWidget { decoration: const BoxDecoration(gradient: TemaApp.gradienteFondo), child: Stack( children: [ + if (intenso) + Positioned.fill( + child: IgnorePointer( + child: Image.asset( + 'assets/ui/generated/gameplay/gameplay_atmosphere_bg.png', + fit: BoxFit.cover, + opacity: const AlwaysStoppedAnimation(0.30), + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) => + const SizedBox.shrink(), + ), + ), + ), Positioned.fill( child: CustomPaint(painter: _FondoFaroleroPainter(intenso: intenso)), ), @@ -383,6 +396,56 @@ class BotonFarolero extends StatelessWidget { } } +class ArteGameplayFarolero extends StatelessWidget { + final String assetPath; + final double height; + final double opacity; + final EdgeInsetsGeometry padding; + + const ArteGameplayFarolero({ + super.key, + required this.assetPath, + this.height = 128, + this.opacity = 0.92, + this.padding = EdgeInsets.zero, + }); + + const ArteGameplayFarolero.fase({ + super.key, + this.height = 128, + this.opacity = 0.92, + this.padding = EdgeInsets.zero, + }) : assetPath = 'assets/ui/generated/gameplay/gameplay_phase_emblem.png'; + + const ArteGameplayFarolero.notas({ + super.key, + this.height = 150, + this.opacity = 0.94, + this.padding = EdgeInsets.zero, + }) : assetPath = 'assets/ui/generated/gameplay/notes_strategy_art.png'; + + @override + Widget build(BuildContext context) { + return ExcludeSemantics( + child: Padding( + padding: padding, + child: SizedBox( + height: height, + width: double.infinity, + child: Image.asset( + assetPath, + fit: BoxFit.contain, + opacity: AlwaysStoppedAnimation(opacity), + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) => + const SizedBox.shrink(), + ), + ), + ), + ); + } +} + class AccesoFarolero extends StatelessWidget { final String etiqueta; final IconData icono; @@ -452,6 +515,16 @@ class TarjetaPalabraFarolero extends StatelessWidget { child: Stack( alignment: Alignment.center, children: [ + Positioned.fill( + child: Image.asset( + 'assets/ui/generated/gameplay/gameplay_phase_emblem.png', + fit: BoxFit.contain, + opacity: const AlwaysStoppedAnimation(0.14), + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) => + const SizedBox.shrink(), + ), + ), Positioned.fill( child: Image.asset( 'assets/ui/premium/word_reveal_glow.png', diff --git a/pubspec.yaml b/pubspec.yaml index 78ec653..52bcc50 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,3 +46,4 @@ flutter: - assets/ui/generated/create_game/ - assets/ui/generated/join_lobby/ - assets/ui/generated/final_rewards/ + - assets/ui/generated/gameplay/ diff --git a/skills/premium-game-ui/SKILL.md b/skills/premium-game-ui/SKILL.md index daaf3c7..74c0c98 100644 --- a/skills/premium-game-ui/SKILL.md +++ b/skills/premium-game-ui/SKILL.md @@ -6,7 +6,7 @@ description: > license: Apache-2.0 metadata: author: gentleman-programming - version: "1.1" + version: "1.2" --- ## When to Use @@ -38,6 +38,8 @@ metadata: 13. **QR codes are functional, not decorative.** A QR must have a clean white quiet zone, enough padding, and no decorative overlay above the scannable pixels. Frames can sit behind or outside the white QR card, never over finder patterns. 14. **Avatars must not be clipped or tiny.** Do not place large avatars in `ListTile.leading` without explicit leading width/height. Use `BoxFit.contain` for generated avatar art, reserve enough tile height, and prefer visibly useful sizes (roughly 48-96 px in lists/cards, 86-112 px in pickers/previews). 15. **Localization is part of premium UI.** User-visible copy must go through the app localization system. For Spanish, use Spanish from Spain; avoid Rioplatense/Argentinian phrasing in app strings. +16. **Do not bundle chroma-source images.** Chroma-key sources are temporary production files. Only the final transparent PNG/WebP and opaque backgrounds belong in registered Flutter asset folders; otherwise green-screen images can be shipped accidentally. +17. **Use shared decorative art widgets for repeated phases.** Debate, word reveal, voting, host-management, notes, rules, and history screens should reuse generated gameplay art through shared components so layouts stay consistent and text/buttons remain real Flutter widgets. ## Mandatory Image Generation Rule