diff --git a/AGENTS.md b/AGENTS.md index c6523d8..ce0d3df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,4 +19,4 @@ | Skill | Description | Path | | --- | --- | --- | -| `premium-game-ui` | Layered premium game UI workflow: transparent overlays, cinematic reward screens, Flutter animation/performance rules. | `skills/premium-game-ui/SKILL.md` | +| `premium-game-ui` | Layered premium game UI workflow: AI-generated high-quality transparent assets, cinematic screens, Flutter animation/performance rules. | `skills/premium-game-ui/SKILL.md` | diff --git a/assets/ui/generated/create_game/.gitkeep b/assets/ui/generated/create_game/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/ui/generated/create_game/create_game_header_art.png b/assets/ui/generated/create_game/create_game_header_art.png new file mode 100644 index 0000000..87e47c9 Binary files /dev/null and b/assets/ui/generated/create_game/create_game_header_art.png differ diff --git a/assets/ui/generated/create_game/create_game_header_art_source_chroma.png b/assets/ui/generated/create_game/create_game_header_art_source_chroma.png new file mode 100644 index 0000000..3ef93fe Binary files /dev/null and b/assets/ui/generated/create_game/create_game_header_art_source_chroma.png differ diff --git a/assets/ui/generated/final_rewards/.gitkeep b/assets/ui/generated/final_rewards/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/ui/generated/join_lobby/.gitkeep b/assets/ui/generated/join_lobby/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/ui/generated/main/.gitkeep b/assets/ui/generated/main/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/ui/generated/main/main_atmosphere_bg.png b/assets/ui/generated/main/main_atmosphere_bg.png new file mode 100644 index 0000000..99fb324 Binary files /dev/null and b/assets/ui/generated/main/main_atmosphere_bg.png differ diff --git a/assets/ui/generated/main/main_cta_frame.png b/assets/ui/generated/main/main_cta_frame.png new file mode 100644 index 0000000..c410c6d Binary files /dev/null and b/assets/ui/generated/main/main_cta_frame.png differ diff --git a/assets/ui/generated/main/main_cta_frame_source_chroma.png b/assets/ui/generated/main/main_cta_frame_source_chroma.png new file mode 100644 index 0000000..8045ad6 Binary files /dev/null and b/assets/ui/generated/main/main_cta_frame_source_chroma.png differ diff --git a/assets/ui/generated/main/main_lantern_hero.png b/assets/ui/generated/main/main_lantern_hero.png new file mode 100644 index 0000000..52163a5 Binary files /dev/null and b/assets/ui/generated/main/main_lantern_hero.png differ diff --git a/assets/ui/generated/main/main_lantern_hero_source_chroma.png b/assets/ui/generated/main/main_lantern_hero_source_chroma.png new file mode 100644 index 0000000..53a20a9 Binary files /dev/null and b/assets/ui/generated/main/main_lantern_hero_source_chroma.png differ diff --git a/assets/ui/generated/mode/.gitkeep b/assets/ui/generated/mode/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/ui/generated/mode/mode_duel_hero.png b/assets/ui/generated/mode/mode_duel_hero.png new file mode 100644 index 0000000..bee2c09 Binary files /dev/null and b/assets/ui/generated/mode/mode_duel_hero.png differ diff --git a/assets/ui/generated/mode/mode_duel_hero_source_chroma.png b/assets/ui/generated/mode/mode_duel_hero_source_chroma.png new file mode 100644 index 0000000..27d8b19 Binary files /dev/null and b/assets/ui/generated/mode/mode_duel_hero_source_chroma.png differ diff --git a/assets/ui/generated/mode/mode_multi_card_frame.png b/assets/ui/generated/mode/mode_multi_card_frame.png new file mode 100644 index 0000000..cd68772 Binary files /dev/null and b/assets/ui/generated/mode/mode_multi_card_frame.png differ diff --git a/assets/ui/generated/mode/mode_multi_card_frame_source_chroma.png b/assets/ui/generated/mode/mode_multi_card_frame_source_chroma.png new file mode 100644 index 0000000..bc6eecb Binary files /dev/null and b/assets/ui/generated/mode/mode_multi_card_frame_source_chroma.png differ diff --git a/assets/ui/generated/mode/mode_single_card_frame.png b/assets/ui/generated/mode/mode_single_card_frame.png new file mode 100644 index 0000000..6757625 Binary files /dev/null and b/assets/ui/generated/mode/mode_single_card_frame.png differ diff --git a/assets/ui/generated/mode/mode_single_card_frame_source_chroma.png b/assets/ui/generated/mode/mode_single_card_frame_source_chroma.png new file mode 100644 index 0000000..e84f71a Binary files /dev/null and b/assets/ui/generated/mode/mode_single_card_frame_source_chroma.png differ diff --git a/assets/ui/generated/shared/.gitkeep b/assets/ui/generated/shared/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/ui/generated/shared/glass_panel_frame.png b/assets/ui/generated/shared/glass_panel_frame.png new file mode 100644 index 0000000..393b162 Binary files /dev/null and b/assets/ui/generated/shared/glass_panel_frame.png differ diff --git a/assets/ui/generated/shared/glass_panel_frame_source_chroma.png b/assets/ui/generated/shared/glass_panel_frame_source_chroma.png new file mode 100644 index 0000000..1cb0513 Binary files /dev/null and b/assets/ui/generated/shared/glass_panel_frame_source_chroma.png differ diff --git a/docs/premium_game_ui_pipeline.md b/docs/premium_game_ui_pipeline.md index 172d54f..583cc0f 100644 --- a/docs/premium_game_ui_pipeline.md +++ b/docs/premium_game_ui_pipeline.md @@ -2,6 +2,24 @@ Objetivo: construir pantallas vistosas sin convertir una maqueta en “un PNG gigante”. La UI debe seguir siendo mantenible, animable, responsive, localizable y performante. + +## Regla obligatoria: arte generado de alta calidad + +Una pantalla premium NO se considera terminada si solo usa gradientes, `CustomPainter`, iconos Material y glows simples. Eso puede servir como scaffolding, pero no alcanza el nivel visual buscado. + +Para cada pantalla o lote premium hay que definir y generar assets artísticos reales: + +| Capa | Asset esperado | Transparencia | +| --- | --- | --- | +| Fondo atmosférico | Imagen generada opaca, móvil vertical, 1080x1920 o similar | No necesaria | +| Hero/emblema/mascota | Imagen generada PNG/WebP de alta calidad | Obligatoria | +| Marco de panel/botón | Imagen generada con bordes/brillos premium | Obligatoria | +| Burst/glow/humo/confetti | Overlay generado pictórico | Obligatoria | +| Iconos/medallas/avatares | 128-256 px o 512 px si son hero | Obligatoria | + +Si el asset va encima de Flutter, debe tener fondo transparente real. Si el generador no entrega alpha nativo, se genera con chroma-key plano, se elimina localmente y se valida que las esquinas tengan `alpha=0`. + +La regla práctica es brutal pero necesaria: **si el resultado parece Flutter con filtros, todavía falta arte**. ## Diagnóstico en Farolero Los assets de `assets/rewards/` mezclan dos usos distintos: diff --git a/docs/professional_generated_ui_art_plan.md b/docs/professional_generated_ui_art_plan.md new file mode 100644 index 0000000..b03992b --- /dev/null +++ b/docs/professional_generated_ui_art_plan.md @@ -0,0 +1,127 @@ +# Professional Generated UI Art Plan + +Este plan corrige el enfoque anterior: una pantalla premium no se da por terminada si solo usa Flutter, gradientes, `CustomPainter`, iconos Material y glows procedurales. Eso es estructura. La dirección visual profesional exige assets generados de alta calidad, integrados por capas. + +## Reglas no negociables + +- La UI real sigue en Flutter: textos, localización, botones, inputs, progreso, navegación y estado. +- El arte visual potente se genera como assets independientes. +- Todo asset que va encima de Flutter debe tener transparencia real: PNG/WebP con alpha. +- Si el generador no entrega alpha nativo, se usa chroma-key plano y eliminación local. +- Se valida tamaño, alpha en esquinas, centrado y peso antes de integrar. +- Se diseña para móvil, tablet, iPad y iPhone: fondos flexibles, contenido con ancho máximo, assets no estirados. + +## Estructura de assets + +```text +assets/ui/generated/ + shared/ + main/ + mode/ + create_game/ + join_lobby/ + final_rewards/ +``` + +## Contrato de tamaños + +| Tipo | Tamaño master | Uso | +| --- | ---: | --- | +| Fondo vertical móvil | 1080×1920 | `BoxFit.cover`, puede ser opaco | +| Fondo tablet | 1440×1920 o 2048×1536 | `BoxFit.cover`, opcional por lote | +| Hero principal | 768–1024 px | PNG transparente | +| Emblema / mascota | 512–1024 px | PNG transparente | +| Marco de card/botón | 1024×256 / 1024×512 | PNG transparente | +| Icono/medalla | 128–256 px | PNG transparente | +| Burst/glow/humo | 512–1024 px | PNG transparente | + +## Manifiesto de assets inicial + +| ID | Ruta | Pantallas | Rol | Alpha | Estado | +| --- | --- | --- | --- | --- | --- | +| `main_atmosphere_bg` | `assets/ui/generated/main/main_atmosphere_bg.png` | Principal | Fondo vertical premium | No | Falta generar | +| `main_lantern_hero` | `assets/ui/generated/main/main_lantern_hero.png` | Principal | Hero/emblema de farol | Sí | Falta generar | +| `main_cta_frame` | `assets/ui/generated/main/main_cta_frame.png` | Principal | Marco/texture CTA | Sí | Falta generar | +| `shared_glass_panel_frame` | `assets/ui/generated/shared/glass_panel_frame.png` | Varias | Marco panel premium | Sí | Falta generar | +| `mode_duel_hero` | `assets/ui/generated/mode/mode_duel_hero.png` | Elegir modo | Hero de decisión | Sí | Falta generar | +| `create_game_header_art` | `assets/ui/generated/create_game/header_art.png` | Crear partida | Cabecera visual | Sí | Falta generar | +| `join_lobby_signal_art` | `assets/ui/generated/join_lobby/signal_art.png` | Unirse/lobby | Arte conexión/QR | Sí | Falta generar | +| `final_rewards_cinematic` | `assets/ui/generated/final_rewards/cinematic_burst.png` | Fin partida | Burst recompensa | Sí | Falta generar | + +## Prompts base + +### Fondo principal + +```text +Use case: stylized-concept +Asset type: mobile game background, opaque +Primary request: high-end cinematic night village background for a social deduction mobile game called Farolero, warm lantern light, mysterious alleys, premium game UI atmosphere, dark navy and amber palette, depth, subtle fog, no text, no UI, no characters, vertical composition. +Size intent: 1080x1920. +Avoid: logos, letters, readable signs, buttons, phone status bar, flat vector look. +``` + +### Hero farol principal transparente + +```text +Use case: stylized-concept +Asset type: transparent hero emblem +Primary request: premium stylized lantern emblem for a mobile social deduction game, cinematic amber glow, polished 3D game icon quality, ornate but readable silhouette, centered, no text. +Transparency: must be isolated on flat chroma-key background if native transparency is unavailable; no shadows baked into the background. +Size intent: 1024x1024. +Avoid: white background, black background, letters, watermark. +``` + +### Marco CTA transparente + +```text +Use case: stylized-concept +Asset type: transparent UI frame +Primary request: premium golden rounded mobile game CTA button frame, glossy amber material, subtle bevel, inner glow, transparent center area usable behind Flutter text, no text, no icons. +Transparency: transparent outside frame and transparent usable center. +Size intent: 1024x256. +Avoid: baked text, opaque rectangle background, excessive decorations. +``` + +## Validación técnica + +Para overlays: + +```powershell +Add-Type -AssemblyName System.Drawing +$bmp=[Drawing.Bitmap]::FromFile("asset.png") +@($bmp.GetPixel(0,0).A,$bmp.GetPixel($bmp.Width-1,0).A,$bmp.GetPixel(0,$bmp.Height-1).A,$bmp.GetPixel($bmp.Width-1,$bmp.Height-1).A) +$bmp.Dispose() +``` + +Resultado esperado: `0,0,0,0` salvo fondos opacos documentados. + +## Rollout + +1. Pantalla principal: corregir dirección visual base. +2. Elegir modo + crear partida: adaptar estilo sin perder usabilidad. +3. Unirse + lobby: QR y conexión como assets reales. +4. Final de partida: cinematográfica de recompensas. +5. Resto de pantallas: aplicar assets compartidos y ajustar responsive. + +## Phase 2 generated assets + +- `assets/ui/generated/mode/mode_duel_hero.png`: hero transparente 1024?1024 para selecci?n de modo. +- `assets/ui/generated/mode/mode_single_card_frame.png`: marco transparente 1400?520 para tarjeta de un m?vil. +- `assets/ui/generated/mode/mode_multi_card_frame.png`: marco transparente 1400?520 para tarjeta multidispositivo. +- `assets/ui/generated/create_game/create_game_header_art.png`: cabecera transparente 1400?620 para crear partida. +- `assets/ui/generated/shared/glass_panel_frame.png`: marco transparente compartido 1400?520 para paneles premium. + +Validaci?n: todos los assets de Phase 2 son PNG RGBA y tienen alpha 0 en las cuatro esquinas. +## Phase 2 correction ? image generation, not procedural assets + +La primera iteraci?n de Phase 2 fue descartada porque los PNG se hab?an producido con Python/Pillow. Eso NO cumple el est?ndar premium: eran composici?n program?tica, no arte generado de alta calidad. + +Assets rehechos con el flujo correcto del skill `imagegen` + `premium-game-ui`: + +- `assets/ui/generated/mode/mode_duel_hero.png` ? generado con `image_gen`, source chroma-key en `mode_duel_hero_source_chroma.png`, alpha removido localmente. +- `assets/ui/generated/mode/mode_single_card_frame.png` ? generado con `image_gen`, source chroma-key en `mode_single_card_frame_source_chroma.png`, alpha removido localmente. +- `assets/ui/generated/mode/mode_multi_card_frame.png` ? generado con `image_gen`, source chroma-key en `mode_multi_card_frame_source_chroma.png`, alpha removido localmente. +- `assets/ui/generated/create_game/create_game_header_art.png` ? generado con `image_gen`, source chroma-key en `create_game_header_art_source_chroma.png`, alpha removido localmente. +- `assets/ui/generated/shared/glass_panel_frame.png` ? generado con `image_gen`, source chroma-key en `glass_panel_frame_source_chroma.png`, alpha removido localmente. + +Validaci?n: todos los PNG finales son RGBA y tienen alpha 0 en las cuatro esquinas. diff --git a/lib/pantallas/pantalla_crear_partida.dart b/lib/pantallas/pantalla_crear_partida.dart index ab9f45c..4c58a0a 100644 --- a/lib/pantallas/pantalla_crear_partida.dart +++ b/lib/pantallas/pantalla_crear_partida.dart @@ -323,9 +323,8 @@ class _PantallaCrearPartidaState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - EncabezadoFarolero( - icono: Icons.groups, - titulo: '¿Cómo quieres jugar?', + _CrearPartidaHeader( + titulo: '?C?mo quieres jugar?', subtitulo: l10n.playersRange, ), const SizedBox(height: 12), @@ -610,3 +609,63 @@ class _PantallaCrearPartidaState extends State { ); } } + +class _CrearPartidaHeader extends StatelessWidget { + final String titulo; + final String subtitulo; + + const _CrearPartidaHeader({ + required this.titulo, + required this.subtitulo, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final ancho = constraints.maxWidth.clamp(320.0, 720.0).toDouble(); + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ancho), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AspectRatio( + aspectRatio: 2, + child: Image.asset( + 'assets/ui/generated/create_game/create_game_header_art.png', + fit: BoxFit.contain, + opacity: const AlwaysStoppedAnimation(0.96), + ), + ), + const SizedBox(height: 8), + Text( + titulo, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: TemaApp.colorDorado, + fontWeight: FontWeight.w900, + shadows: [ + Shadow( + color: TemaApp.colorNaranja.withValues(alpha: 0.55), + blurRadius: 16, + ), + ], + ), + ), + const SizedBox(height: 4), + Text( + subtitulo, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: TemaApp.colorTextoSecundario, + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pantallas/pantalla_principal.dart b/lib/pantallas/pantalla_principal.dart index 1172723..d4bcc99 100644 --- a/lib/pantallas/pantalla_principal.dart +++ b/lib/pantallas/pantalla_principal.dart @@ -1,5 +1,3 @@ -import 'dart:math' as math; - import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -31,8 +29,12 @@ class PantallaPrincipal extends StatelessWidget { intenso: true, child: Stack( children: [ - const Positioned.fill( - child: IgnorePointer(child: CustomPaint(painter: _InicioFondoPainter())), + Positioned.fill( + child: Image.asset( + 'assets/ui/generated/main/main_atmosphere_bg.png', + fit: BoxFit.cover, + alignment: Alignment.center, + ), ), Positioned.fill( child: IgnorePointer( @@ -53,92 +55,108 @@ class PantallaPrincipal extends StatelessWidget { ), ), SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 430), - child: Column( - children: [ - _PerfilInicioPremium( - nombre: perfil.nombre, - nick: perfil.nick, - avatarAsset: perfil.avatarAsset, - fuego: gamificacion.fuego, - medallas: gamificacion.medallas, - onAjustes: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const PantallaAjustes(), - ), - ); - }, - ajustesTooltip: l10n.settings, - ).animate().fadeIn(duration: 280.ms).slideY(begin: -0.12), - const SizedBox(height: 42), - _HeroInicioPremium(subtitulo: l10n.subtitle) - .animate() - .fadeIn(delay: 120.ms, duration: 420.ms) - .scale(begin: const Offset(0.92, 0.92)), - const SizedBox(height: 46), - _BotonInicioPremium.primario( - texto: 'Jugar', - icono: Icons.play_arrow_rounded, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const PantallaSeleccionModoJuego(), - ), - ); - }, - ).animate().fadeIn(delay: 240.ms).slideY(begin: 0.16), - const SizedBox(height: 14), - _BotonInicioPremium.secundario( - texto: l10n.joinGame, - icono: Icons.bolt_rounded, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const PantallaUnirse()), - ); - }, - ).animate().fadeIn(delay: 320.ms).slideY(begin: 0.16), - const SizedBox(height: 12), - _BotonInicioPremium.oscuro( - texto: l10n.howToPlay, - icono: Icons.question_mark_rounded, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const PantallaReglas()), - ); - }, - ).animate().fadeIn(delay: 390.ms).slideY(begin: 0.16), - const SizedBox(height: 14), - _AccesoHistorialPremium( - etiqueta: 'Historial', - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const PantallaHistorial()), - ); - }, - ).animate().fadeIn(delay: 470.ms).slideY(begin: 0.14), - const SizedBox(height: 28), - Text( - l10n.playersRange, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: TemaApp.colorTextoSecundario.withValues(alpha: 0.82), - letterSpacing: 0.8, - ), + child: LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth >= 700; + final maxContentWidth = isWide ? 540.0 : 430.0; + final horizontalPadding = isWide ? 32.0 : 20.0; + + return Center( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + horizontalPadding, + 18, + horizontalPadding, + 24, + ), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxContentWidth), + child: Column( + children: [ + _PerfilInicioPremium( + nombre: perfil.nombre, + nick: perfil.nick, + avatarAsset: perfil.avatarAsset, + fuego: gamificacion.fuego, + medallas: gamificacion.medallas, + onAjustes: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const PantallaAjustes(), + ), + ); + }, + ajustesTooltip: l10n.settings, + ).animate().fadeIn(duration: 280.ms).slideY(begin: -0.12), + SizedBox(height: isWide ? 48 : 34), + _HeroInicioPremium( + subtitulo: l10n.subtitle, + compact: constraints.maxHeight < 760, + ) + .animate() + .fadeIn(delay: 120.ms, duration: 420.ms) + .scale(begin: const Offset(0.92, 0.92)), + SizedBox(height: isWide ? 46 : 34), + _BotonInicioPremium.primario( + texto: 'Jugar', + icono: Icons.play_arrow_rounded, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const PantallaSeleccionModoJuego(), + ), + ); + }, + ).animate().fadeIn(delay: 240.ms).slideY(begin: 0.16), + const SizedBox(height: 14), + _BotonInicioPremium.secundario( + texto: l10n.joinGame, + icono: Icons.bolt_rounded, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const PantallaUnirse()), + ); + }, + ).animate().fadeIn(delay: 320.ms).slideY(begin: 0.16), + const SizedBox(height: 12), + _BotonInicioPremium.oscuro( + texto: l10n.howToPlay, + icono: Icons.question_mark_rounded, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const PantallaReglas()), + ); + }, + ).animate().fadeIn(delay: 390.ms).slideY(begin: 0.16), + const SizedBox(height: 14), + _AccesoHistorialPremium( + etiqueta: 'Historial', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const PantallaHistorial()), + ); + }, + ).animate().fadeIn(delay: 470.ms).slideY(begin: 0.14), + const SizedBox(height: 28), + Text( + l10n.playersRange, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: TemaApp.colorTextoSecundario.withValues(alpha: 0.82), + letterSpacing: 0.8, + ), + ), + ], ), - ], + ), ), - ), - ), + ); + }, ), ), ], @@ -252,56 +270,45 @@ class _PerfilInicioPremium extends StatelessWidget { class _HeroInicioPremium extends StatelessWidget { final String subtitulo; + final bool compact; - const _HeroInicioPremium({required this.subtitulo}); + const _HeroInicioPremium({ + required this.subtitulo, + required this.compact, + }); @override Widget build(BuildContext context) { + final heroSize = compact ? 148.0 : 188.0; + return SizedBox( - height: 230, + height: compact ? 260 : 320, child: Stack( alignment: Alignment.center, children: [ - const Positioned.fill( - child: IgnorePointer(child: CustomPaint(painter: _HeroInicioPainter())), + Positioned.fill( + child: Image.asset( + 'assets/ui/premium/lantern_radial_glow.png', + fit: BoxFit.contain, + opacity: const AlwaysStoppedAnimation(0.48), + ), ), Column( mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 92, - height: 92, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - TemaApp.colorDorado.withValues(alpha: 0.92), - TemaApp.colorNaranja.withValues(alpha: 0.55), - Colors.black.withValues(alpha: 0.78), - ], - ), - border: Border.all(color: TemaApp.colorDorado, width: 3), - boxShadow: [ - BoxShadow( - color: TemaApp.colorNaranja.withValues(alpha: 0.65), - blurRadius: 42, - spreadRadius: 7, - ), - ], - ), - child: const Icon( - Icons.lightbulb_rounded, - color: Color(0xFF251304), - size: 54, - ), + Image.asset( + 'assets/ui/generated/main/main_lantern_hero.png', + width: heroSize, + height: heroSize, + fit: BoxFit.contain, ).animate(onPlay: (controller) => controller.repeat(reverse: true)).scale( - begin: const Offset(0.98, 0.98), - end: const Offset(1.04, 1.04), - duration: 1400.ms, + begin: const Offset(0.985, 0.985), + end: const Offset(1.035, 1.035), + duration: 1800.ms, curve: Curves.easeInOut, ), - const SizedBox(height: 12), - const LogoFarolero(size: 64), + SizedBox(height: compact ? 8 : 12), + LogoFarolero(size: compact ? 58 : 72), const SizedBox(height: 8), Text( subtitulo, @@ -320,7 +327,7 @@ class _HeroInicioPremium extends StatelessWidget { ), const SizedBox(height: 4), Text( - 'Descubr� al impostor antes de que sea tarde', + 'Descubrí al impostor antes de que sea tarde', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: TemaApp.colorTextoSecundario, @@ -437,23 +444,44 @@ class _BotonInicioPremium extends StatelessWidget { ), ], ), - child: Row( - children: [ - SizedBox(width: hero ? 70 : 62, child: Icon(icono, color: foreground, size: hero ? 38 : 27)), - Expanded( - child: Text( - texto.toUpperCase(), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge?.copyWith( + child: ClipRRect( + borderRadius: BorderRadius.circular(hero ? 26 : 22), + child: Stack( + children: [ + if (hero) + Positioned.fill( + child: Image.asset( + 'assets/ui/generated/main/main_cta_frame.png', + fit: BoxFit.fill, + ), + ), + Row( + children: [ + SizedBox( + width: hero ? 70 : 62, + child: Icon( + icono, color: foreground, - fontSize: hero ? 28 : 18, - fontWeight: FontWeight.w900, - letterSpacing: hero ? 1.6 : 1.0, + size: hero ? 38 : 27, ), + ), + Expanded( + child: Text( + texto.toUpperCase(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: foreground, + fontSize: hero ? 28 : 18, + fontWeight: FontWeight.w900, + letterSpacing: hero ? 1.6 : 1.0, + ), + ), + ), + SizedBox(width: hero ? 70 : 62), + ], ), - ), - SizedBox(width: hero ? 70 : 62), - ], + ], + ), ), ), ), @@ -523,105 +551,3 @@ BoxDecoration _decoracionCristal({required double radius, required double alpha} ], ); } - -class _InicioFondoPainter extends CustomPainter { - const _InicioFondoPainter(); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..isAntiAlias = true; - paint.shader = const LinearGradient( - colors: [Color(0xFF050A12), Color(0xFF0A1524), Color(0xFF15091D)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ).createShader(Offset.zero & size); - canvas.drawRect(Offset.zero & size, paint); - - final farol = Offset(size.width * 0.5, size.height * 0.34); - paint.shader = RadialGradient( - colors: [ - TemaApp.colorDorado.withValues(alpha: 0.28), - TemaApp.colorNaranja.withValues(alpha: 0.12), - Colors.transparent, - ], - ).createShader(Rect.fromCircle(center: farol, radius: size.width * 0.78)); - canvas.drawCircle(farol, size.width * 0.78, paint); - paint.shader = null; - - _drawSkyline(canvas, size, paint); - _drawSparks(canvas, size); - } - - void _drawSkyline(Canvas canvas, Size size, Paint paint) { - paint.color = Colors.black.withValues(alpha: 0.38); - final base = size.height * 0.80; - final path = Path() - ..moveTo(0, size.height) - ..lineTo(0, base - 20) - ..lineTo(size.width * 0.14, base - 58) - ..lineTo(size.width * 0.26, base - 24) - ..lineTo(size.width * 0.42, base - 78) - ..lineTo(size.width * 0.58, base - 34) - ..lineTo(size.width * 0.74, base - 74) - ..lineTo(size.width * 0.90, base - 28) - ..lineTo(size.width, base - 48) - ..lineTo(size.width, size.height) - ..close(); - canvas.drawPath(path, paint); - - paint.color = TemaApp.colorNaranja.withValues(alpha: 0.18); - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromCenter(center: Offset(size.width * 0.5, base - 108), width: 24, height: 150), - const Radius.circular(12), - ), - paint, - ); - } - - void _drawSparks(Canvas canvas, Size size) { - final palette = [TemaApp.colorDorado, TemaApp.colorNaranja, const Color(0xFFFFF2C9)]; - for (var i = 0; i < 64; i++) { - final x = (i * 67 % math.max(size.width, 1)).toDouble(); - final y = (i * 113 % math.max(size.height, 1)).toDouble(); - final paint = Paint() - ..isAntiAlias = true - ..color = palette[i % palette.length].withValues(alpha: 0.12 + (i % 4) * 0.06); - canvas.drawCircle(Offset(x, y), 1.2 + (i % 3), paint); - } - } - - @override - bool shouldRepaint(covariant _InicioFondoPainter oldDelegate) => false; -} - -class _HeroInicioPainter extends CustomPainter { - const _HeroInicioPainter(); - - @override - void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height * 0.45); - final paint = Paint()..isAntiAlias = true; - paint.shader = RadialGradient( - colors: [TemaApp.colorNaranja.withValues(alpha: 0.30), Colors.transparent], - ).createShader(Rect.fromCircle(center: center, radius: size.width * 0.54)); - canvas.drawCircle(center, size.width * 0.54, paint); - paint.shader = null; - - for (var i = 0; i < 24; i++) { - final angle = math.pi * 2 * i / 24; - paint.color = (i.isEven ? TemaApp.colorDorado : TemaApp.colorNaranja).withValues(alpha: i.isEven ? 0.14 : 0.08); - canvas.drawPath( - Path() - ..moveTo(center.dx + math.cos(angle - 0.035) * 45, center.dy + math.sin(angle - 0.035) * 45) - ..lineTo(center.dx + math.cos(angle) * size.width * 0.44, center.dy + math.sin(angle) * size.width * 0.44) - ..lineTo(center.dx + math.cos(angle + 0.035) * 45, center.dy + math.sin(angle + 0.035) * 45) - ..close(), - paint, - ); - } - } - - @override - bool shouldRepaint(covariant _HeroInicioPainter oldDelegate) => false; -} diff --git a/lib/pantallas/pantalla_seleccion_modo_juego.dart b/lib/pantallas/pantalla_seleccion_modo_juego.dart index 9d1d840..03a9ba6 100644 --- a/lib/pantallas/pantalla_seleccion_modo_juego.dart +++ b/lib/pantallas/pantalla_seleccion_modo_juego.dart @@ -28,6 +28,7 @@ class PantallaSeleccionModoJuego extends StatelessWidget { const _ModoHero().animate().fadeIn(duration: 320.ms).slideY(begin: -0.12), const SizedBox(height: 34), _ModoCard( + marcoAsset: 'assets/ui/generated/mode/mode_single_card_frame.png', icono: Icons.phone_android_rounded, titulo: 'Un móvil', subtitulo: 'Partida en este dispositivo', @@ -44,6 +45,7 @@ class PantallaSeleccionModoJuego extends StatelessWidget { ).animate().fadeIn(delay: 120.ms).slideX(begin: -0.08), const SizedBox(height: 16), _ModoCard( + marcoAsset: 'assets/ui/generated/mode/mode_multi_card_frame.png', icono: Icons.devices_rounded, titulo: 'Multidispositivo', subtitulo: 'Cada jugador en su móvil', @@ -75,74 +77,45 @@ class _ModoHero extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - height: 230, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: Image.asset( - 'assets/ui/premium/lantern_radial_glow.png', - fit: BoxFit.contain, - opacity: const AlwaysStoppedAnimation(0.58), - ), + return Column( + children: [ + SizedBox( + height: 230, + child: Image.asset( + 'assets/ui/generated/mode/mode_duel_hero.png', + fit: BoxFit.contain, + opacity: const AlwaysStoppedAnimation(0.95), ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 90, - height: 90, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - TemaApp.colorDorado.withValues(alpha: 0.95), - TemaApp.colorNaranja.withValues(alpha: 0.58), - Colors.black.withValues(alpha: 0.76), - ], - ), - border: Border.all(color: TemaApp.colorDorado, width: 3), - boxShadow: [ - BoxShadow( - color: TemaApp.colorNaranja.withValues(alpha: 0.55), - blurRadius: 42, - spreadRadius: 5, - ), - ], - ), - child: const Icon(Icons.sports_esports_rounded, size: 48, color: Color(0xFF241103)), + ), + const SizedBox(height: 10), + Text( + '?C?mo quer?s jugar?', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: TemaApp.colorDorado, + fontSize: 32, + fontWeight: FontWeight.w900, + shadows: [ + Shadow(color: TemaApp.colorNaranja.withValues(alpha: 0.45), blurRadius: 16), + ], ), - const SizedBox(height: 18), - Text( - '¿Cómo querés jugar?', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: TemaApp.colorDorado, - fontSize: 32, - fontWeight: FontWeight.w900, - shadows: [ - Shadow(color: TemaApp.colorNaranja.withValues(alpha: 0.45), blurRadius: 16), - ], - ), + ), + const SizedBox(height: 8), + Text( + 'Eleg? el tipo de partida y arranc? sin fricci?n.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: TemaApp.colorTextoSecundario, ), - const SizedBox(height: 8), - Text( - 'Elegí el tipo de partida y arrancá sin fricción.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: TemaApp.colorTextoSecundario, - ), - ), - ], - ), - ], - ), + ), + ], ); } + } class _ModoCard extends StatelessWidget { + final String marcoAsset; final IconData icono; final String titulo; final String subtitulo; @@ -151,6 +124,7 @@ class _ModoCard extends StatelessWidget { final VoidCallback onTap; const _ModoCard({ + required this.marcoAsset, required this.icono, required this.titulo, required this.subtitulo, @@ -189,6 +163,16 @@ class _ModoCard extends StatelessWidget { ), child: Stack( children: [ + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(28), + child: Image.asset( + marcoAsset, + fit: BoxFit.fill, + opacity: const AlwaysStoppedAnimation(0.86), + ), + ), + ), Positioned.fill( child: Image.asset( 'assets/ui/premium/card_sheen_overlay.png', diff --git a/pubspec.yaml b/pubspec.yaml index 02ba116..d2f3ec6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,3 +39,4 @@ flutter: - assets/medals/ - assets/rewards/ - assets/ui/premium/ + - assets/ui/generated/ diff --git a/skills/premium-game-ui/SKILL.md b/skills/premium-game-ui/SKILL.md index 7ee2f15..848808a 100644 --- a/skills/premium-game-ui/SKILL.md +++ b/skills/premium-game-ui/SKILL.md @@ -1,53 +1,83 @@ --- name: premium-game-ui description: > - Create professional, layered, high-impact game screens in Flutter without flattening the UI into a single image. - Trigger: when designing, generating, reviewing, or implementing premium game UI screens, reward screens, medal screens, avatar screens, overlays, transparent assets, visual effects, or cinematic game feedback. + Create professional, layered, high-impact game screens in Flutter using AI-generated, high-quality transparent art assets without flattening the UI into a single image. + Trigger: when designing, generating, reviewing, or implementing premium game UI screens, reward screens, medal screens, avatar screens, overlays, transparent assets, visual effects, cinematic game feedback, or mockup-to-Flutter work. license: Apache-2.0 metadata: author: gentleman-programming - version: "1.0" + version: "1.1" --- ## When to Use -- A screen must look premium, cinematic, or game-like. +- A screen must look premium, cinematic, addictive, or game-like. - Assets are being generated for Flutter UI. - The user mentions transparent backgrounds, overlays, rewards, medals, avatars, glows, bursts, particles, confetti, Lottie, Rive, or visual polish. - A prototype/mockup must be converted into real app UI. +- A screen currently looks like plain Flutter plus gradients/glows and needs real art direction. ## Critical Patterns -1. **Never flatten a real screen into one PNG.** Use real Flutter layout for text, buttons, state, progress, localization, and accessibility. -2. **Generate a layered asset kit.** Separate opaque backgrounds from transparent overlays. -3. **Transparent means technically transparent.** For overlays, image corners should usually be `alpha=0`; reject white/black baked backgrounds. -4. **Right size, not maximum size.** Icons/avatars usually 128–256 px. Hero bursts 512–1024 px. Full backgrounds can be larger. -5. **Use animation by responsibility.** +1. **Never flatten a real screen into one PNG.** Use real Flutter layout for text, buttons, state, progress, localization, accessibility, and responsive behavior. +2. **Do not fake premium art with simple procedural glows.** If the target is cinematic/professional, generate real high-quality raster art assets with the image generation system and integrate them. CustomPainter, gradients, and stock Material icons are support layers, not hero art. +3. **Generate a layered asset kit before coding.** Separate opaque backgrounds from transparent overlays, hero illustrations, frames, badges, buttons, bursts, particles, smoke, and decorative props. +4. **Transparent means technically transparent.** Every overlay/hero/frame/prop that sits above Flutter UI must be PNG/WebP with alpha; image corners should usually be `alpha=0`. Reject white/black baked backgrounds. +5. **High-quality generated assets are mandatory for premium screens.** For each premium screen or lote, create or reuse at least one production-grade generated art asset unless the screen is intentionally text-only. +6. **Right size, not maximum size.** Icons/avatars/medals usually 128-256 px. Hero/foreground illustrations 512-1024 px. Full mobile backgrounds can be 1080x1920 or equivalent. +7. **Use animation by responsibility.** - `flutter_animate`: widget entrances, shimmer, scale, blur, staggered UI. - `confetti`: short celebrations; keep particles controlled. - `Rive`: interactive/stateful vector animation. - `Lottie`: After Effects JSON animations. -6. **Avoid expensive composition.** Do not wrap large areas in `Opacity` when a semitransparent color/image or pre-baked alpha asset works. -7. **Plan before generating.** First write the layer list: filename, role, size, alpha requirement, anchor, and Flutter usage. +8. **Avoid expensive composition.** Do not wrap large areas in `Opacity` when a semitransparent color/image or pre-baked alpha asset works. +9. **Plan before generating.** First write the layer list: filename, role, size, alpha requirement, anchor, and Flutter usage. +10. **No placeholders in final premium work.** Generic icons, basic circles, simple radial glows, plain gradients, and procedural sparkles are acceptable only as temporary scaffolding. Replace them with generated art assets before claiming the screen is done. + +## Mandatory Image Generation Rule + +For premium/cinematic screens, the implementation MUST use generated bitmap assets where visual impact matters. + +| Need | Required output | +| --- | --- | +| Full-screen atmosphere | Generated opaque background, usually 1080x1920 or 1440x2560. | +| Hero symbol / mascot / emblem | Generated PNG/WebP with transparent background, usually 512-1024 px. | +| Card/button frame art | Generated PNG/WebP with transparent background and alpha corners. | +| Reward burst / glow / confetti / smoke | Generated PNG/WebP with alpha, corners transparent. | +| Small icon/medal/avatar | Generated or hand-authored asset at 128-256 px, transparent background. | + +If an asset needs transparency and the generator cannot emit native alpha, generate it on a flat chroma-key background, remove the key locally, and validate alpha before integration. ## Asset Acceptance Checklist -- [ ] Correct role: background, overlay, hero element, icon, or animation. +- [ ] Correct role: background, overlay, hero element, icon, frame, badge, prop, or animation. - [ ] Correct format: PNG/WebP alpha for pictorial overlays, SVG for simple vector, Rive/Lottie for motion. - [ ] Correct alpha: overlay corners transparent unless intentionally full-canvas. +- [ ] Generated art quality: looks like polished game art, not a procedural placeholder. +- [ ] Project-bound generated images are copied into `assets/ui/...` or another registered asset directory. - [ ] Centered visible content; no neighboring sprite fragments. - [ ] No unnecessary 2048+ px icons. - [ ] Registered in `pubspec.yaml` only at the needed directory level. - [ ] Integrated through `Stack`/widgets, not as a full-screen screenshot. +- [ ] Compared against the mockup/prototype; if the result still looks like plain Flutter with glows, it is not done. + +## Screen Workflow + +1. Define the screen as layers: background, atmosphere, hero, panels, controls, motion. +2. Generate the required high-quality image assets first. +3. Remove/challenge any opaque background on overlays; validate transparent corners. +4. Integrate assets with real Flutter widgets in `Stack`. +5. Compare against the prototype and iterate asset-by-asset. +6. Only then update the screen code and verification notes. ## Flutter Integration Pattern ```dart Stack( children: [ - const PremiumBackground(), // opaque base - Positioned.fill(child: Image.asset('assets/fx/vignette.png', fit: BoxFit.cover)), - Positioned(top: 80, left: 0, right: 0, child: RewardHero()), + const PremiumBackground(), // opaque generated or painted base + Positioned.fill(child: Image.asset('assets/ui/premium/vignette.png', fit: BoxFit.cover)), + Positioned(top: 80, left: 0, right: 0, child: RewardHero()), // generated transparent hero asset + Flutter text/state Positioned.fill(child: IgnorePointer(child: ConfettiWidget(confettiController: controller))), SafeArea(child: RealScreenContent()), // text/buttons/progress remain widgets ],