import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/network/ws_client.dart'; import '../../domain/entities/game_entities.dart'; import '../../domain/repository/game_repository.dart'; import 'game_event.dart'; import 'game_state.dart'; /// بلوکِ realtime بازی: به استریمِ پیام‌ها/وضعیتِ repository گوش می‌دهد و /// اقدام‌های بازیکن را به سرور می‌فرستد. متدهای کمکی برای موتور Flame هم دارد. class GameBloc extends Bloc { final GameRepository repository; late final StreamSubscription _msgSub; late final StreamSubscription _statusSub; Map _joinAction = const {}; bool _joined = false; int _bubbleSeq = 0; // شناسه‌ی یکتا برای هر حبابِ پیام (انقضای دقیق) GameBloc(this.repository) : super(const GameUiState()) { _msgSub = repository.messages.listen((m) => add(GameMessageReceived(m))); _statusSub = repository.status.listen((s) => add(GameStatusChanged(s))); on((event, emit) async { _joinAction = event.joinAction; _joined = false; await repository.connect(); }); on((event, emit) { emit(state.copyWith(connection: event.status)); if (event.status == WsStatus.connected && !_joined) { _joined = true; repository.send(_joinAction); } }); on((event, emit) => _onMessage(event.message, emit)); on( (event, emit) => repository.send({'type': 'choose_trump', 'suit': event.suit})); on( (event, emit) => repository.send({'type': 'play_card', 'card': event.card})); on((event, emit) => repository.send({'type': 'reshuffle'})); on((event, emit) => repository.send({ 'type': 'chat', if (event.text != null) 'text': event.text, if (event.emoji != null) 'emoji': event.emoji, })); on((event, emit) { final cur = state.chatBubbles[event.seat]; if (cur == null || cur.id != event.id) return; // حبابِ جدیدتر است ⇒ نگه‌دار final next = Map.from(state.chatBubbles) ..remove(event.seat); emit(state.copyWith(chatBubbles: next)); }); on((event, emit) => repository.send({'type': 'leave'})); on((event, emit) => repository.send({'type': 'start_table', 'hands': event.hands})); on((event, emit) => repository.send({'type': 'rotate_table', 'dir': event.dir})); on((event, emit) => repository.send({'type': 'leave_table'})); on((event, emit) => emit(state.copyWith(clearNotice: true))); } void _onMessage(Map msg, Emitter emit) { switch (msg['type']) { case 'state': final gs = GameState.fromJson(msg); final clear = gs.phase == 'choose_trump' || gs.phase == 'playing'; emit(state.copyWith(state: gs, clearHandResult: clear)); case 'hand_over': emit(state.copyWith(handResult: HandResult.fromJson(msg))); case 'game_over': emit(state.copyWith(gameOver: GameOver.fromJson(msg))); case 'table_lobby': emit(state.copyWith(lobby: TableLobby.fromJson(msg))); case 'countdown': emit(state.copyWith(countdown: (msg['seconds'] ?? 3) as int)); case 'table_closed': emit(state.copyWith( tableClosed: true, notice: 'میز توسط میزبان بسته شد')); case 'player_disconnected': emit(state.copyWith(notice: 'یک بازیکن قطع شد')); case 'player_reconnected': emit(state.copyWith(notice: 'بازیکن بازگشت')); case 'chat': final seat = (msg['seat'] ?? 0) as int; final id = ++_bubbleSeq; final bubble = ChatBubble( text: msg['text'] as String?, emoji: msg['emoji'] as String?, id: id, ); final next = Map.from(state.chatBubbles) ..[seat] = bubble; emit(state.copyWith(chatBubbles: next)); // انقضای خودکار پس از ۴ ثانیه (مگر حبابِ جدیدتری روی همان جایگاه بنشیند). Timer(const Duration(seconds: 4), () => add(ExpireChatEvent(seat, id))); case 'player_left': emit(state.copyWith(notice: 'یک بازیکن میز را ترک کرد')); case 'error': emit(state.copyWith(notice: (msg['message'] ?? 'خطا').toString())); } } // --- متدهای کمکی برای موتور Flame و صفحه‌ها --- void chooseTrump(String suit) => add(ChooseTrumpEvent(suit)); void reshuffle() => add(ReshuffleEvent()); void sendChatText(String text) => add(SendChatEvent(text: text)); void sendChatEmoji(String emoji) => add(SendChatEvent(emoji: emoji)); void playCard(String card) => add(PlayCardEvent(card)); void leave() => add(LeaveGameEvent()); void startTable(int hands) => add(StartTableEvent(hands)); void rotateTable(int dir) => add(RotateTableEvent(dir)); void leaveTable() => add(LeaveTableEvent()); void clearNotice() => add(ClearNoticeEvent()); @override Future close() { _msgSub.cancel(); _statusSub.cancel(); repository.disconnect(); return super.close(); } }