// اوورلی‌های خصوصیِ صفحه‌ی بازی (بنرها، پنلِ جستجو، انتخابِ حکم، نتیجه‌ی دست/بازی). part of 'game_screen.dart'; /// بنرِ «نوبت شماست» وقتی کاربر باید کارت بیندازد. class _TurnBanner extends StatelessWidget { const _TurnBanner(); @override Widget build(BuildContext context) => Align( alignment: const Alignment(0, 0.3), child: IgnorePointer( child: Container( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.gold, AppColors.goldDark], ), borderRadius: BorderRadius.circular(20), boxShadow: const [ BoxShadow(color: Colors.black54, blurRadius: 8) ], ), child: const Text( 'نوبت شماست', style: TextStyle( color: AppColors.bg, fontWeight: FontWeight.bold, fontSize: 16, ), ), ), ), ); } /// بنرِ قطعِ ارتباط با سرور (بالای صفحه). class _ConnBanner extends StatelessWidget { const _ConnBanner(); @override Widget build(BuildContext context) => Positioned( top: 0, left: 0, right: 0, child: Material( color: Colors.orange.shade900, child: const SafeArea( bottom: false, child: Padding( padding: EdgeInsets.all(8), child: Text( 'ارتباط با سرور قطع شد، در حال تلاش برای اتصال مجدد…', textAlign: TextAlign.center, style: TextStyle(color: Colors.white), ), ), ), ), ); } /// نشانِ درون‌بازیِ تورنومنت: می‌فهماند این بازیِ عمومی به تورنومنت امتیاز می‌دهد. class _TournamentBadge extends StatelessWidget { final String title; const _TournamentBadge({required this.title}); @override Widget build(BuildContext context) => Positioned( top: 6, left: 0, right: 0, child: IgnorePointer( child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF3A2A6E), AppColors.lapisInk], ), borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.gold, width: 1.2), boxShadow: [ BoxShadow( color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 8), ], ), child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.emoji_events, color: AppColors.gold, size: 15), const SizedBox(width: 5), Text( 'تورنومنت: $title', style: const TextStyle( color: AppColors.gold, fontSize: 12, fontWeight: FontWeight.bold), ), ]), ), ), ), ); } /// پنلِ «جستجوی حریف»: چهار جایگاه + وضعیتِ جستجو یا شمارشِ معکوسِ شروع. class _SearchPanel extends StatelessWidget { final GameUiState state; final int countdown; const _SearchPanel({required this.state, required this.countdown}); @override Widget build(BuildContext context) { final players = state.state?.players ?? const []; final mySeat = state.state?.yourSeat ?? -1; final searching = state.state == null; GamePlayer? at(int seat) { for (final p in players) { if (p.seat == seat) return p; } return null; } return Container( color: Colors.black.withValues(alpha: 0.78), alignment: Alignment.center, child: Container( margin: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.fromLTRB(16, 14, 16, 18), decoration: BoxDecoration( color: AppColors.panel, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.gold, width: 2.5), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'جستجوی حریف', style: TextStyle( color: AppColors.gold, fontSize: 22, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 18), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var seat = 0; seat < 4; seat++) _SearchSlot( player: at(seat), isYou: seat == mySeat, seat: seat, ), ], ), if (searching) ...[ const SizedBox(height: 16), const Text( 'در حال یافتن حریفان…', style: TextStyle(color: Colors.white70, fontSize: 13), ), ] else if (countdown > 0) ...[ const SizedBox(height: 14), const Text( 'شروع بازی', style: TextStyle(color: Colors.white70, fontSize: 13), ), const SizedBox(height: 6), TweenAnimationBuilder( // هر عدد با یک «پاپ» ظاهر می‌شود تا واضح و جلب‌توجه باشد. key: ValueKey(countdown), tween: Tween(begin: 1.6, end: 1.0), duration: const Duration(milliseconds: 400), curve: Curves.easeOut, builder: (_, scale, child) => Transform.scale(scale: scale, child: child), child: Container( width: 54, height: 54, alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, gradient: const LinearGradient( colors: [AppColors.gold, AppColors.goldDark], ), border: Border.all(color: Colors.white, width: 2), ), child: Text( '$countdown', style: const TextStyle( color: AppColors.bg, fontSize: 26, fontWeight: FontWeight.bold, ), ), ), ), ], ], ), ), ); } } /// یک جایگاه در پنلِ جستجو (نام، آواتار/آیکن، برچسبِ نقش). class _SearchSlot extends StatelessWidget { final GamePlayer? player; final bool isYou; final int seat; const _SearchSlot({ required this.player, required this.isYou, required this.seat, }); @override Widget build(BuildContext context) { final p = player; final found = p != null; return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( found ? p.name : '...', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: isYou ? AppColors.gold : Colors.white70, fontSize: 12, ), ), const SizedBox(height: 4), Container( width: 58, height: 58, padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.bgDark, borderRadius: BorderRadius.circular(10), border: Border.all( color: isYou ? AppColors.gold : AppColors.goldDark, width: 1.5, ), ), child: !found ? _SearchingAvatar(slot: seat) // آواتارهای متغیر و متفاوت : p.bot ? const Icon( Icons.smart_toy, color: AppColors.gold, size: 30, ) : ClipRRect( borderRadius: BorderRadius.circular(8), child: PlayerAvatar( seed: p.avatarSeed, imageUrl: p.avatarImg, ), ), ), const SizedBox(height: 2), Text( found ? (p.bot ? 'ربات' : (isYou ? 'شما' : 'حریف')) : '', style: const TextStyle(color: Colors.white38, fontSize: 10), ), ], ), ); } } /// دیالوگِ انتخابِ حکم توسطِ حاکم (+ بُرِ مجدد با مصرفِ یک بلیت). class _TrumpPicker extends StatelessWidget { const _TrumpPicker(); @override Widget build(BuildContext context) { const suits = [ ('spades', '♠', 'پیک', Colors.white), ('hearts', '♥', 'دل', AppColors.suitRed), ('diamonds', '♦', 'خشت', AppColors.suitRed), ('clubs', '♣', 'گشنیز', Colors.white), ]; return Container( color: Colors.black54, alignment: Alignment.center, child: Container( margin: const EdgeInsets.all(24), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.panel, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.gold, width: 2), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'حکم را انتخاب کن', style: TextStyle(color: AppColors.gold, fontSize: 20), ), const SizedBox(height: 16), Wrap( spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, children: [ for (final (id, sym, name, color) in suits) ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppColors.bgDark, minimumSize: const Size(120, 64), ), onPressed: () => context.read().chooseTrump(id), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(sym, style: TextStyle(fontSize: 26, color: color)), const SizedBox(width: 8), Text( name, style: const TextStyle(color: AppColors.text), ), ], ), ), ], ), const SizedBox(height: 16), const Divider(color: AppColors.goldFaint, height: 1), const SizedBox(height: 12), // بُرِ مجدد با مصرفِ یک بلیت (۵ کارتِ تازه، سپس باز هم انتخابِ حکم). BlocBuilder( builder: (context, ws) { final tickets = ws.walletStatus is WalletLoaded ? (ws.walletStatus as WalletLoaded).wallet.tickets : 0; final canReshuffle = tickets > 0; return Column( children: [ ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF8E1B7A), disabledBackgroundColor: const Color(0xFF3A2233), minimumSize: const Size(220, 48), ), onPressed: canReshuffle ? () { AppSounds.button(); context.read().reshuffle(); // پس از مصرفِ بلیت، موجودی را تازه کن. final wallet = context.read(); Future.delayed(const Duration(milliseconds: 500), () => wallet.add(LoadWalletEvent())); } : null, icon: const Icon(Icons.casino, color: Colors.white), label: const Text( 'بُر مجدد (۱ بلیت)', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 4), Text( canReshuffle ? 'بلیت‌های شما: $tickets' : 'بلیت کافی ندارید', style: const TextStyle(color: Colors.white60, fontSize: 12), ), ], ); }, ), ], ), ), ); } } /// نتیجه‌ی یک دست با بازخوردِ فوری: آیکنِ بزرگِ رنگی (جام/ضربدر)، واژه‌ی بزرگِ /// سبز/قرمز، واشِ رنگیِ لبه‌های صفحه و «پاپِ» ورودی تا کاربر بی‌درنگ بفهمد بُرد یا /// باخت. کُتِ بُرده جشنِ ویژه (کانفتیِ بیشتر + عنوانِ آتشین) دارد. class _HandResult extends StatelessWidget { final GameUiState state; const _HandResult({required this.state}); // سبزِ بُرد / قرمزِ باخت (منبعِ رنگِ واش، حاشیه و هاله). static const _winGreen = Color(0xFF3FA34D); static const _loseRed = Color(0xFFE5484D); @override Widget build(BuildContext context) { final r = state.handResult!; final myTeam = (state.state?.yourSeat ?? 0) % 2; final won = r.winnerTeam == myTeam; final kotWin = won && r.kot; // جشنِ ویژه فقط برای کُتِ بُرده final accent = won ? _winGreen : _loseRed; final mine = myTeam < r.scores.length ? r.scores[myTeam] : 0; final opp = (1 - myTeam) < r.scores.length ? r.scores[1 - myTeam] : 0; final title = kotWin ? (r.hakemKot ? 'حاکم‌کُت! 🔥' : 'کُت! 🔥') : (won ? 'بردید!' : 'باختید'); return IgnorePointer( child: Stack( children: [ // ۱) واشِ رنگیِ لبه‌ها (سبز=بُرد، قرمز=باخت) که سریع ظاهر می‌شود ⇒ درکِ آنی // حتی با نگاهِ گوشه‌ی چشم. Positioned.fill( child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 300), builder: (_, v, __) => DecoratedBox( decoration: BoxDecoration( gradient: RadialGradient( radius: 1.15, colors: [ Colors.transparent, accent.withValues(alpha: 0.30 * v), ], stops: const [0.5, 1.0], ), ), ), ), ), // ۲) کانفتی برای هر بُرد (کُت پرحجم‌تر). if (won) Positioned.fill(child: Confetti(count: kotWin ? 48 : 22)), // ۳) نشانِ مرکزی با «پاپ» ورودی. Center( child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 500), curve: Curves.easeOutBack, builder: (_, v, child) => Transform.scale( scale: v.clamp(0.0, 1.12), child: Opacity(opacity: v.clamp(0.0, 1.0), child: child), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // آیکنِ بزرگِ رنگی: جامِ سبز برای بُرد، ضربدرِ قرمز برای باخت. Container( width: 96, height: 96, alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: won ? const [Color(0xFF5FD36B), Color(0xFF1B7A2E)] : const [Color(0xFFF06A6E), Color(0xFF8E1B1F)], ), border: Border.all(color: Colors.white, width: 3), boxShadow: [ BoxShadow( color: accent.withValues(alpha: 0.65), blurRadius: 26, spreadRadius: 2), ], ), child: Icon( won ? Icons.emoji_events_rounded : Icons.close_rounded, color: Colors.white, size: 52, ), ), const SizedBox(height: 14), // واژه‌ی بزرگِ نتیجه. Text( title, style: TextStyle( color: won ? AppColors.gold : const Color(0xFFF06A6E), fontSize: 40, fontWeight: FontWeight.bold, shadows: const [ Shadow(color: Colors.black, blurRadius: 12) ], ), ), if (kotWin) const Text( 'این دست را بردید', style: TextStyle( color: Color(0xFF7BE58A), fontSize: 15, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 10), // چیپِ امتیاز/نتیجه (از دیدِ کاربر: ما · حریف). Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.55), borderRadius: BorderRadius.circular(14), border: Border.all(color: accent.withValues(alpha: 0.7)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (won) ...[ Text( '+${r.points} امتیاز', style: const TextStyle( color: AppColors.gold, fontWeight: FontWeight.bold), ), Container( width: 1, height: 15, margin: const EdgeInsets.symmetric(horizontal: 10), color: Colors.white24, ), ], Text( 'ما $mine · حریف $opp', style: const TextStyle( color: Colors.white70, fontWeight: FontWeight.bold), ), ], ), ), ], ), ), ), // ۴) عددِ امتیاز از پنلِ امتیاز (بالا-چپ) با پرواز به نشانِ نتیجه می‌رسد // تا کاربر پیوندِ امتیازِ میز با نتیجه‌ی دست را ببیند — چه بُرد چه باخت. _ScoreFly(points: r.points, won: won), ], ), ); } } /// عددِ امتیازِ این دست که از پنلِ امتیاز (بالا-چپِ صفحه) به‌سمتِ نشانِ نتیجه /// (مرکز) پرواز می‌کند و آن‌جا محو می‌شود — پیوندِ بصریِ امتیازِ میز با نتیجه. /// بُرد: طلایی از سمتِ «ما»؛ باخت: قرمز از سمتِ «حریف». class _ScoreFly extends StatelessWidget { final int points; final bool won; const _ScoreFly({required this.points, required this.won}); @override Widget build(BuildContext context) { // نقطه‌ی شروع نزدیکِ عددِ مربوطه در پنلِ بالا-چپ («ما» راست‌تر، «حریف» چپ‌تر). final start = won ? const Alignment(-0.68, -0.92) : const Alignment(-0.9, -0.92); final grad = won ? const [Color(0xFFFFE082), AppColors.goldDark] : const [Color(0xFFF06A6E), Color(0xFF8E1B1F)]; final textColor = won ? AppColors.bg : Colors.white; final glow = won ? AppColors.gold : const Color(0xFFF06A6E); return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 850), curve: Curves.easeInOut, builder: (context, t, child) { // مسیرِ حرکت: از عددِ پنل تا کمی بالای نشانِ نتیجه. final align = Alignment.lerp(start, const Alignment(0, -0.22), t)!; // نزدیکِ مقصد بزرگ‌تر می‌شود و در انتها محو می‌شود (به نشان می‌پیوندد). final scale = 0.85 + 0.6 * t; final fade = t < 0.82 ? 1.0 : (1 - (t - 0.82) / 0.18); return Align( alignment: align, child: Opacity( opacity: fade.clamp(0.0, 1.0), child: Transform.scale(scale: scale, child: child), ), ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), decoration: BoxDecoration( gradient: LinearGradient(colors: grad), borderRadius: BorderRadius.circular(22), border: Border.all(color: Colors.white, width: 1.5), boxShadow: [ BoxShadow(color: glow.withValues(alpha: 0.75), blurRadius: 16), ], ), child: Text( '+$points', style: TextStyle( color: textColor, fontWeight: FontWeight.bold, fontSize: 21, ), ), ), ); } } /// اوورلیِ پایانِ بازی (مدال، نتیجه، پاداش‌ها، بازگشت به لابی). class _GameOver extends StatelessWidget { final GameUiState state; final int prize; final String? tournamentTitle; final VoidCallback onExit; const _GameOver({ required this.state, required this.prize, required this.tournamentTitle, required this.onExit, }); @override Widget build(BuildContext context) { final g = state.gameOver!; final mySeat = state.state?.yourSeat ?? 0; final won = g.winnerTeam == mySeat % 2; return Stack( children: [ const Positioned.fill(child: ColoredBox(color: Colors.black87)), if (won) const Positioned.fill(child: Confetti()), Center( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ // مدالِ متحرک (تاجِ طلایی برای برد، نشانِ ملایم برای باخت). TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 650), curve: Curves.easeOutBack, builder: (_, v, __) => Transform.scale( scale: v.clamp(0.0, 1.2), child: Container( width: 100, height: 100, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: won ? const [AppColors.gold, AppColors.goldDark] : [Colors.grey.shade700, Colors.grey.shade900], ), border: Border.all( color: won ? Colors.white70 : Colors.white24, width: 2), boxShadow: won ? [ BoxShadow( color: AppColors.gold.withValues(alpha: 0.55), blurRadius: 28), ] : null, ), child: Icon( won ? Icons.emoji_events : Icons.sentiment_dissatisfied_outlined, color: won ? AppColors.lapisInk : Colors.white54, size: 54, ), ), ), ), const SizedBox(height: 16), Text( won ? 'بردید! 🎉' : 'باختید', style: TextStyle( color: won ? AppColors.gold : Colors.redAccent, fontSize: 34, fontWeight: FontWeight.bold, shadows: const [Shadow(color: Colors.black, blurRadius: 8)], ), ), const SizedBox(height: 10), Text( 'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}', style: const TextStyle(color: Colors.white70, fontSize: 18), ), // پاداشِ سکه‌ی برد (جایزه‌ی میز). if (won && prize > 0) ...[ const SizedBox(height: 16), _RewardChip( icon: Icons.monetization_on, text: '+$prize سکه'), ], // امتیازِ کسب‌شده‌ی تورنومنت (برد ۱۰۰، شرکت ۲۵). if (tournamentTitle != null) ...[ const SizedBox(height: 12), _RewardChip( icon: Icons.emoji_events, text: '+${won ? 100 : 25} امتیاز تورنومنت «$tournamentTitle»'), ], const SizedBox(height: 30), SizedBox( width: 220, child: ElevatedButton( onPressed: onExit, child: const Text('بازگشت به لابی'), ), ), ], ), ), ), ], ); } } /// چیپِ پاداش (آیکن + متن) در اوورلیِ پایانِ بازی. class _RewardChip extends StatelessWidget { final IconData icon; final String text; const _RewardChip({required this.icon, required this.text}); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: AppColors.gold.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.gold), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: AppColors.gold, size: 20), const SizedBox(width: 8), Text(text, style: const TextStyle( color: AppColors.gold, fontSize: 14, fontWeight: FontWeight.bold)), ]), ); } /// آواتارِ در حالِ تغییر برای جایگاه‌های خالیِ پنل جستجو (حسِ «در حال یافتن»). class _SearchingAvatar extends StatefulWidget { final int slot; // هر جایگاه دامنه‌ی seedِ متفاوت دارد تا یکسان نباشند const _SearchingAvatar({required this.slot}); @override State<_SearchingAvatar> createState() => _SearchingAvatarState(); } class _SearchingAvatarState extends State<_SearchingAvatar> { Timer? _timer; int _i = 0; @override void initState() { super.initState(); _i = widget.slot * 13; // شروعِ متفاوت برای هر جایگاه _timer = Timer.periodic(const Duration(milliseconds: 240), (_) { if (mounted) setState(() => _i++); }); } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { // پیشوندِ مخصوصِ جایگاه ⇒ آواتارِ هر جایگاه با بقیه فرق دارد. return ClipRRect( borderRadius: BorderRadius.circular(8), child: RandomAvatar('s${widget.slot}-$_i'), ); } }