import 'dart:async'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show HapticFeedback; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:random_avatar/random_avatar.dart'; import '../../../../core/locator/locator.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/service/app_sounds.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../shop/data/data_source/remote/frame_api_provider.dart'; import '../../../tournament/data/data_source/remote/tournament_api_provider.dart'; import '../../../tournament/domain/entities/tournament.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart'; import '../../../wallet/presentation/bloc/wallet_event.dart'; import '../../../wallet/presentation/bloc/wallet_state.dart'; import '../../../wallet/presentation/bloc/wallet_status.dart'; import '../../domain/entities/game_entities.dart'; import '../bloc/game_bloc.dart'; import '../bloc/game_state.dart'; import '../../../chat/presentation/widgets/chat_panel.dart'; import '../widgets/confetti.dart'; import '../widgets/flame/hokm_game.dart'; import '../widgets/table_hud.dart'; /// صفحه‌ی میز بازی: صحنه‌ی Flame + اوورلی‌های وضعیت. class GameScreen extends StatefulWidget { final int prize; const GameScreen({super.key, this.prize = 0}); @override State createState() => _GameScreenState(); } class _GameScreenState extends State { late final HokmGame _game; String _carpet = 'classic'; // فرشِ انتخابی (برای پس‌زمینه‌ی پشتِ صحنه) Timer? _introTimer; bool _introHidden = false; int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱) Tournament? _tournament; // تورنومنتِ فعالِ ثبت‌نام‌شده (اگر باشد) — برای نشانِ درون‌بازی @override void initState() { super.initState(); final ws = context.read().state.walletStatus; final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple'; _carpet = ws is WalletLoaded ? ws.wallet.selectedCarpet : 'classic'; _game = HokmGame( context.read(), skin: skin.isEmpty ? 'simple' : skin, carpet: _carpet.isEmpty ? 'classic' : _carpet, ); _loadTournament(); locator().warmCache(); // رنگِ قاب‌ها برای HUD (best-effort) } /// بررسی می‌کند آیا کاربر در تورنومنتِ فعالی ثبت‌نام کرده تا نشانِ درون‌بازی و /// امتیازِ پایانِ بازی نمایش داده شود (این بازیِ عمومی به تورنومنت امتیاز می‌دهد). Future _loadTournament() async { try { final list = await locator().getTournaments(); final active = list.where((t) => t.joined && t.isActive).toList(); if (mounted && active.isNotEmpty) { setState(() => _tournament = active.first); } } catch (_) {/* بی‌اهمیت */} } @override void dispose() { _introTimer?.cancel(); super.dispose(); } bool _showSearch(GameUiState s) => s.state == null || !_introHidden; @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (!didPop) _confirmLeave(context); }, child: SafeArea( top: false, child: Scaffold( body: BlocConsumer( listenWhen: (a, b) => (a.notice != b.notice && b.notice != null) || (a.gameOver == null && b.gameOver != null) || (a.handResult == null && b.handResult != null), listener: (context, state) { // جشنِ کُت: هنگامِ دستِ کُت‌شده‌ی بُرده، ضربه‌ی لمسی. final hr = state.handResult; if (hr != null && hr.kot) { final myTeam = (state.state?.yourSeat ?? 0) % 2; if (hr.winnerTeam == myTeam) HapticFeedback.mediumImpact(); } if (state.gameOver != null) { final won = state.gameOver!.winnerTeam == (state.state?.yourSeat ?? 0) % 2; if (won) { AppSounds.win(); HapticFeedback.mediumImpact(); } } if (state.notice != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.notice!), duration: const Duration(seconds: 2), ), ); context.read().clearNotice(); // خطا پیش از شروعِ بازی (مثلاً سکه‌ی ناکافی) ⇒ کاربر در دیالوگِ // «جستجوی حریف» گیر نکند؛ به لابی برگردد. if (state.state == null && state.gameOver == null) { _exitToLobby(context); } } }, builder: (context, state) { if (state.state != null && _introTimer == null) { // حریفان پیدا شدند ⇒ شمارشِ معکوسِ ۳…۲…۱ با صدا تا کاربر بداند // بازی در حالِ شروع است، سپس اوورلی کنار می‌رود و بُر زدن اجرا می‌شود. _countdown = 3; // داخلِ build هستیم؛ همین build مقدار را نشان می‌دهد AppSounds.tick(); _introTimer = Timer.periodic(const Duration(seconds: 1), (t) { if (!mounted) { t.cancel(); return; } if (_countdown <= 1) { t.cancel(); setState(() => _introHidden = true); _game.endIntro(); } else { setState(() => _countdown--); AppSounds.tick(); } }); } return Stack( children: [ // فرش به‌عنوانِ سطحِ میز داخلِ صحنه‌ی Flame (Felt) کشیده می‌شود. GameWidget(game: _game), if (!_showSearch(state) && state.state != null) TableHud( s: state.state!, connection: state.connection, chatBubbles: state.chatBubbles, onExit: () => _confirmLeave(context), onChat: () => _openChat(context), ), if (state.connection == WsStatus.disconnected) _connBanner(), if (_tournament != null && !_showSearch(state)) _tournamentBadge(), if (_showSearch(state)) _searchPanel(state), // دیالوگِ انتخابِ حکم و بنرِ نوبت فقط پس از پایانِ انیمیشنِ // پخشِ کارت نمایش داده می‌شوند (نه وسطِ بُر زدن/پخش). if (!_showSearch(state)) ValueListenableBuilder( valueListenable: _game.animating, builder: (context, animating, _) { if (animating) return const SizedBox.shrink(); return Stack( children: [ if (_showTrumpPicker(state)) _trumpPicker(context), if (_isMyPlayTurn(state)) _turnBanner(), ], ); }, ), if (state.handResult != null && state.gameOver == null) _handResult(state), if (state.gameOver != null) _gameOver(context, state), ], ); }, ), ), ), ); } void _openChat(BuildContext context) { final bloc = context.read(); final wallet = context.read(); ChatPanel.show( context, onText: bloc.sendChatText, onEmoji: bloc.sendChatEmoji, onWalletChanged: () => wallet.add(LoadWalletEvent()), ); } bool _isMyPlayTurn(GameUiState s) => s.state != null && s.gameOver == null && s.state!.phase == 'playing' && s.state!.isMyTurn && !s.state!.trickDone; // بنرِ «نوبت شماست» وقتی کاربر باید کارت بیندازد. Widget _turnBanner() => 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, ), ), ), ), ); bool _showTrumpPicker(GameUiState s) => s.state != null && s.state!.phase == 'choose_trump' && s.state!.amHakem && s.gameOver == null; Future _confirmLeave(BuildContext context) async { if (context.read().state.gameOver != null) { _exitToLobby(context); return; } final yes = await showDialog( context: context, builder: (_) => AlertDialog( backgroundColor: AppColors.panel, title: const Text('خروج از میز'), content: const Text( 'از میز خارج می‌شوید؟ ورودی بازگردانده نمی‌شود.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('ماندن'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('خروج'), ), ], ), ); if (yes == true && context.mounted) { context.read().leave(); _exitToLobby(context); } } void _exitToLobby(BuildContext context) { _game.silence(); // پس از خروج، صدای کارت‌های بات پخش نشود context.read().add(LoadWalletEvent()); context.go('/lobby'); } Widget _connBanner() => 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), ), ), ), ), ); Widget _searchPanel(GameUiState state) { 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(at(seat), seat == mySeat, 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, ), ), ), ), ], ], ), ), ); } Widget _searchSlot(GamePlayer? p, bool isYou, int seat) { 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: RandomAvatar(p.avatarSeed), ), ), const SizedBox(height: 2), Text( found ? (p.bot ? 'ربات' : (isYou ? 'شما' : 'حریف')) : '', style: const TextStyle(color: Colors.white38, fontSize: 10), ), ], ), ); } Widget _trumpPicker(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(); // پس از مصرفِ بلیت، موجودی را تازه کن (بدون نگه‌داشتنِ context). 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), ), ], ); }, ), ], ), ), ); } Widget _handResult(GameUiState s) { final r = s.handResult!; final mySeat = s.state?.yourSeat ?? 0; final myTeam = mySeat % 2; final won = r.winnerTeam == myTeam; final kotWin = won && r.kot; // جشنِ ویژه فقط برای کُتِ بُرده return IgnorePointer( child: Stack( children: [ if (kotWin) const Positioned.fill(child: Confetti(count: 40)), Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 18), decoration: BoxDecoration( gradient: kotWin ? const LinearGradient( colors: [AppColors.lapisMid, AppColors.lapisInk]) : null, color: kotWin ? null : Colors.black87, borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.gold, width: kotWin ? 2 : 1), boxShadow: kotWin ? [ BoxShadow( color: AppColors.gold.withValues(alpha: 0.4), blurRadius: 20), ] : null, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (kotWin) Text( r.hakemKot ? 'حاکم‌کُت! 🔥' : 'کُت! 🔥', style: const TextStyle( color: AppColors.gold, fontSize: 30, fontWeight: FontWeight.bold, shadows: [Shadow(color: Colors.black, blurRadius: 8)], ), ), Text( won ? 'این دست را بردید!' : 'این دست را باختید', style: TextStyle( color: won ? AppColors.green : Colors.redAccent, fontSize: kotWin ? 16 : 20, fontWeight: FontWeight.bold, ), ), if (r.kot && !kotWin) Padding( padding: const EdgeInsets.only(top: 6), child: Text( r.hakemKot ? 'حاکم‌کُت! (${r.points} امتیاز)' : 'کُت! (${r.points} امتیاز)', style: const TextStyle(color: AppColors.gold), ), ), if (kotWin) Text('+${r.points} امتیاز', style: const TextStyle(color: AppColors.gold)), const SizedBox(height: 6), Text( 'امتیاز: ${r.scores.isNotEmpty ? r.scores[0] : 0} - ${r.scores.length > 1 ? r.scores[1] : 0}', style: const TextStyle(color: Colors.white70), ), ], ), ), ), ], ), ); } // نشانِ درون‌بازیِ تورنومنت: به کاربر می‌فهماند این بازیِ عمومی به تورنومنت // امتیاز می‌دهد (بالای صحنه، وسط). Widget _tournamentBadge() => 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( 'تورنومنت: ${_tournament!.title}', style: const TextStyle( color: AppColors.gold, fontSize: 12, fontWeight: FontWeight.bold), ), ]), ), ), ), ); Widget _gameOver(BuildContext context, GameUiState s) { final g = s.gameOver!; final mySeat = s.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 && widget.prize > 0) ...[ const SizedBox(height: 16), _rewardChip( Icons.monetization_on, '+${widget.prize} سکه'), ], // امتیازِ کسب‌شده‌ی تورنومنت (برد ۱۰۰، شرکت ۲۵). if (_tournament != null) ...[ const SizedBox(height: 12), _rewardChip(Icons.emoji_events, '+${won ? 100 : 25} امتیاز تورنومنت «${_tournament!.title}»'), ], const SizedBox(height: 30), SizedBox( width: 220, child: ElevatedButton( onPressed: () => _exitToLobby(context), child: const Text('بازگشت به لابی'), ), ), ], ), ), ), ], ); } Widget _rewardChip(IconData icon, String text) => 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'), ); } }