264 lines
10 KiB
Dart
264 lines
10 KiB
Dart
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<GameScreen> createState() => _GameScreenState();
|
|
}
|
|
|
|
class _GameScreenState extends State<GameScreen> {
|
|
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<WalletBloc>().state.walletStatus;
|
|
final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple';
|
|
_carpet = ws is WalletLoaded ? ws.wallet.selectedCarpet : 'classic';
|
|
_game = HokmGame(
|
|
context.read<GameBloc>(),
|
|
skin: skin.isEmpty ? 'simple' : skin,
|
|
carpet: _carpet.isEmpty ? 'classic' : _carpet,
|
|
);
|
|
_loadTournament();
|
|
locator<FrameApiProvider>().warmCache(); // رنگِ قابها برای HUD (best-effort)
|
|
}
|
|
|
|
/// بررسی میکند آیا کاربر در تورنومنتِ فعالی ثبتنام کرده تا نشانِ درونبازی و
|
|
/// امتیازِ پایانِ بازی نمایش داده شود (این بازیِ عمومی به تورنومنت امتیاز میدهد).
|
|
Future<void> _loadTournament() async {
|
|
try {
|
|
final list = await locator<TournamentApiProvider>().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<GameBloc, GameUiState>(
|
|
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<GameBloc>().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<bool>(
|
|
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<GameBloc>();
|
|
final wallet = context.read<WalletBloc>();
|
|
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<void> _confirmLeave(BuildContext context) async {
|
|
if (context.read<GameBloc>().state.gameOver != null) {
|
|
_exitToLobby(context);
|
|
return;
|
|
}
|
|
final yes = await showDialog<bool>(
|
|
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<GameBloc>().leave();
|
|
_exitToLobby(context);
|
|
}
|
|
}
|
|
|
|
void _exitToLobby(BuildContext context) {
|
|
_game.silence(); // پس از خروج، صدای کارتهای بات پخش نشود
|
|
context.read<WalletBloc>().add(LoadWalletEvent());
|
|
context.go('/lobby');
|
|
}
|
|
|
|
}
|