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'; part 'game_screen.parts.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) const _ConnBanner(), if (_tournament != null && !_showSearch(state)) _TournamentBadge(title: _tournament!.title), if (_showSearch(state)) _SearchPanel(state: state, countdown: _countdown), // دیالوگِ انتخابِ حکم و بنرِ نوبت فقط پس از پایانِ انیمیشنِ // پخشِ کارت نمایش داده می‌شوند (نه وسطِ بُر زدن/پخش). if (!_showSearch(state)) ValueListenableBuilder( valueListenable: _game.animating, builder: (context, animating, _) { if (animating) return const SizedBox.shrink(); return Stack( children: [ if (_showTrumpPicker(state)) const _TrumpPicker(), if (_isMyPlayTurn(state)) const _TurnBanner(), ], ); }, ), if (state.handResult != null && state.gameOver == null) _HandResult(state: state), if (state.gameOver != null) _GameOver( state: state, prize: widget.prize, tournamentTitle: _tournament?.title, onExit: () => _exitToLobby(context), ), ], ); }, ), ), ), ); } 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; 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'); } }