import 'dart:async'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:random_avatar/random_avatar.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/service/app_sounds.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart'; import '../../../wallet/presentation/bloc/wallet_event.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 '../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; Timer? _introTimer; bool _introHidden = false; @override void initState() { super.initState(); final ws = context.read().state.walletStatus; final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple'; _game = HokmGame( context.read(), skin: skin.isEmpty ? 'simple' : skin, ); } @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), listener: (context, state) { if (state.gameOver != null) { final won = state.gameOver!.winnerTeam == (state.state?.yourSeat ?? 0) % 2; if (won) AppSounds.win(); } 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) { // کمی «حریفان پیدا شد» نشان بده، بعد اوورلی را کنار بزن و همان // لحظه انیمیشنِ بُر زدن (با صدا) را اجرا کن. _introTimer = Timer(const Duration(milliseconds: 1000), () { if (!mounted) return; setState(() => _introHidden = true); _game.endIntro(); }); } return Stack( children: [ GameWidget(game: _game), if (!_showSearch(state) && state.state != null) TableHud( s: state.state!, connection: state.connection, onExit: () => _confirmLeave(context), onChat: () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('گفتگو به‌زودی'), duration: Duration(seconds: 1), ), ), ), if (state.connection == WsStatus.disconnected) _connBanner(), 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), ], ); }, ), ), ), ); } 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.80), child: IgnorePointer( child: Container( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFFE9B949), Color(0xFFB8860B)], ), borderRadius: BorderRadius.circular(20), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 8)], ), child: const Text( 'نوبت شماست', style: TextStyle( color: Color(0xFF3A0A12), 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), ), ], ], ), ), ); } 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.name), ), ), 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', '♥', 'دل', Color(0xFFD32F2F)), ('diamonds', '♦', 'خشت', Color(0xFFD32F2F)), ('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), ), ], ), ), ], ), ], ), ), ); } Widget _handResult(GameUiState s) { final r = s.handResult!; final mySeat = s.state?.yourSeat ?? 0; final myTeam = mySeat % 2; final won = r.winnerTeam == myTeam; return IgnorePointer( child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 18), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.gold), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( won ? 'این دست را بردید!' : 'این دست را باختید', style: TextStyle( color: won ? AppColors.green : Colors.redAccent, fontSize: 20, fontWeight: FontWeight.bold, ), ), if (r.kot) Padding( padding: const EdgeInsets.only(top: 6), child: Text( r.hakemKot ? 'حاکم‌کُت! (${r.points} امتیاز)' : 'کُت! (${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 _gameOver(BuildContext context, GameUiState s) { final g = s.gameOver!; final mySeat = s.state?.yourSeat ?? 0; final won = g.winnerTeam == mySeat % 2; return Container( color: Colors.black87, alignment: Alignment.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( won ? 'بردید! 🎉' : 'باختید', style: TextStyle( color: won ? AppColors.gold : Colors.redAccent, fontSize: 32, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), Text( 'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}', style: const TextStyle(color: Colors.white70, fontSize: 18), ), const SizedBox(height: 28), SizedBox( width: 220, child: ElevatedButton( onPressed: () => _exitToLobby(context), child: const Text('بازگشت به لابی'), ), ), ], ), ); } } /// آواتارِ در حالِ تغییر برای جایگاه‌های خالیِ پنل جستجو (حسِ «در حال یافتن»). 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'), ); } }