From 04b97ca16344f2d09fb808756824db73b680b163 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Fri, 19 Jun 2026 21:35:23 +0330 Subject: [PATCH] priority of cards fixed --- .../game/presentation/screen/game_screen.dart | 32 +++++++---- .../presentation/widgets/flame/hokm_game.dart | 57 +++++++++++++++++-- .../widgets/flame/shuffle_animation.dart | 46 ++++++++++----- 3 files changed, 106 insertions(+), 29 deletions(-) diff --git a/lib/feature/game/presentation/screen/game_screen.dart b/lib/feature/game/presentation/screen/game_screen.dart index 8207e62..014db6a 100644 --- a/lib/feature/game/presentation/screen/game_screen.dart +++ b/lib/feature/game/presentation/screen/game_screen.dart @@ -97,12 +97,21 @@ class _GameScreenState extends State { _backButton(context), if (state.connection == WsStatus.disconnected) _connBanner(), if (_showSearch(state)) _searchPanel(state), - if (!_showSearch(state) && _showTrumpPicker(state)) - _trumpPicker(context), + // دیالوگِ انتخابِ حکم و بنرِ نوبت فقط پس از پایانِ انیمیشنِ + // پخشِ کارت نمایش داده می‌شوند (نه وسطِ بُر زدن/پخش). + 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 (!_showSearch(state) && _isMyPlayTurn(state)) - _turnBanner(), if (state.gameOver != null) _gameOver(context, state), ], ); @@ -258,7 +267,7 @@ class _GameScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ for (var seat = 0; seat < 4; seat++) - _searchSlot(at(seat), seat == mySeat), + _searchSlot(at(seat), seat == mySeat, seat), ], ), if (searching) ...[ @@ -274,7 +283,7 @@ class _GameScreenState extends State { ); } - Widget _searchSlot(GamePlayer? p, bool isYou) { + Widget _searchSlot(GamePlayer? p, bool isYou, int seat) { final found = p != null; return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), @@ -305,7 +314,7 @@ class _GameScreenState extends State { ), child: !found - ? const _SearchingAvatar() // آواتارهای متغیر تا یافتنِ حریف + ? _SearchingAvatar(slot: seat) // آواتارهای متغیر و متفاوت : p.bot ? const Icon( Icons.smart_toy, @@ -471,7 +480,8 @@ class _GameScreenState extends State { /// آواتارِ در حالِ تغییر برای جایگاه‌های خالیِ پنل جستجو (حسِ «در حال یافتن»). class _SearchingAvatar extends StatefulWidget { - const _SearchingAvatar(); + final int slot; // هر جایگاه دامنه‌ی seedِ متفاوت دارد تا یکسان نباشند + const _SearchingAvatar({required this.slot}); @override State<_SearchingAvatar> createState() => _SearchingAvatarState(); @@ -484,7 +494,8 @@ class _SearchingAvatarState extends State<_SearchingAvatar> { @override void initState() { super.initState(); - _timer = Timer.periodic(const Duration(milliseconds: 220), (_) { + _i = widget.slot * 13; // شروعِ متفاوت برای هر جایگاه + _timer = Timer.periodic(const Duration(milliseconds: 240), (_) { if (mounted) setState(() => _i++); }); } @@ -497,9 +508,10 @@ class _SearchingAvatarState extends State<_SearchingAvatar> { @override Widget build(BuildContext context) { + // پیشوندِ مخصوصِ جایگاه ⇒ آواتارِ هر جایگاه با بقیه فرق دارد. return ClipRRect( borderRadius: BorderRadius.circular(8), - child: RandomAvatar('seek-${_i % 24}'), + child: RandomAvatar('s${widget.slot}-$_i'), ); } } diff --git a/lib/feature/game/presentation/widgets/flame/hokm_game.dart b/lib/feature/game/presentation/widgets/flame/hokm_game.dart index c6bc0fc..0624a82 100644 --- a/lib/feature/game/presentation/widgets/flame/hokm_game.dart +++ b/lib/feature/game/presentation/widgets/flame/hokm_game.dart @@ -49,9 +49,16 @@ class HokmGame extends FlameGame { bool _introActive = true; // تا پایانِ نمایشِ «جستجوی حریف» bool _pendingShuffle = false; // بُرِ دستِ اول که تا پایانِ اینترو به تعویق افتاده - bool _shuffling = false; // در حالِ پخشِ انیمیشنِ بُر زدن (بازیِ کارت ممنوع) + bool _shuffling = false; // در حالِ بُر زدن/پخشِ ۵ کارت (بازیِ کارت ممنوع) + bool _dealing = false; // در حالِ پخشِ بقیه‌ی کارت‌ها پس از حکم (بازی ممنوع) String? _prevTrump; // برای تشخیصِ لحظه‌ی انتخابِ حکم + /// آیا انیمیشنی در حال اجراست (اینترو/بُر زدن/پخشِ کارت). صفحه از این برای + /// پنهان‌کردنِ دیالوگِ انتخابِ حکم تا پایانِ پخشِ کارت‌ها استفاده می‌کند. + final ValueNotifier animating = ValueNotifier(true); + void _refreshAnimating() => + animating.value = _introActive || _shuffling || _dealing; + // مدیریتِ ضربانِ قلب (فقط نوبتِ خودِ کاربر؛ مستقل از بازساختِ صحنه). double _turnElapsed = 0; bool _beatPlayed = false; // یک‌بار در هر نوبت، هنگام رسیدن به آستانه @@ -83,6 +90,7 @@ class HokmGame extends FlameGame { if (_prevTrump == null && s.trump != null && !_silent) { AppSounds.selectHokm(); _spawnTrumpReveal(s.trump!); + _dealAllNow(); // پخشِ بقیه‌ی کارت‌ها (بازی تا پایانِ آن قفل است) } _prevTrump = s.trump; _prevHandSize = s.yourHand.length; @@ -117,7 +125,8 @@ class HokmGame extends FlameGame { // شمشیر + تکان + رعد)؛ خالی‌شدنِ زمین ⇒ صدای جمع‌شدنِ دست. void _checkTrickSounds(GameState s) { final now = s.trick.map((t) => t.card).toList(); - if (_silent) { + // در زمانِ اینترو/بُر زدن/پخش، کارتی روی میز نشان داده نمی‌شود ⇒ صدا هم نه. + if (_silent || _introActive || _shuffling || _dealing) { _prevTrick = now; return; } @@ -157,6 +166,7 @@ class HokmGame extends FlameGame { _pendingShuffle = false; _shuffleNow(); } + _refreshAnimating(); } /// اجرای هم‌زمانِ انیمیشن و صدای بُر زدن (صدا با پایانِ انیمیشن قطع می‌شود). @@ -166,12 +176,28 @@ class HokmGame extends FlameGame { AppSounds.shuffle(cutoffMs: ShuffleAnimation.totalMs); add(ShuffleAnimation(size)); _shuffling = true; + _refreshAnimating(); Future.delayed(const Duration(milliseconds: ShuffleAnimation.totalMs), () { _shuffling = false; + _refreshAnimating(); if (_s != null && !_silent) _relayout(); // فعال‌سازیِ دوباره‌ی کارت‌ها }); } + /// پخشِ بقیه‌ی کارت‌ها پس از انتخابِ حکم؛ تا پایانِ انیمیشن بازیِ کارت قفل است. + void _dealAllNow() { + if (_silent) return; + AppSounds.shuffle(cutoffMs: ShuffleAnimation.dealMs); + add(ShuffleAnimation(size, dealOnly: true)); + _dealing = true; + _refreshAnimating(); + Future.delayed(const Duration(milliseconds: ShuffleAnimation.dealMs), () { + _dealing = false; + _refreshAnimating(); + if (_s != null && !_silent) _relayout(); + }); + } + /// نمایشِ بزرگ و گذرای حکمِ انتخاب‌شده در مرکزِ میز (با پاپ). void _spawnTrumpReveal(String trump) { final (sym, col) = _trumpGlyph(trump); @@ -286,8 +312,8 @@ class HokmGame extends FlameGame { // آیا این کارت در نوبتِ فعلی قابل بازی است (با رعایت follow-suit)؟ bool _legal(String code) { final s = _s!; - // هنگامِ نمایشِ اینترو یا انیمیشنِ بُر زدن، بازیِ کارت مجاز نیست. - if (_introActive || _shuffling) return false; + // هنگامِ اینترو/بُر زدن/پخشِ کارت‌ها، بازیِ کارت مجاز نیست. + if (_introActive || _shuffling || _dealing) return false; if (s.phase != 'playing' || s.trickDone || !s.isMyTurn) return false; final lead = s.leadSuit; if (lead == null || lead.isEmpty) return true; @@ -317,6 +343,15 @@ class HokmGame extends FlameGame { Rect.fromLTRB(size.x * 0.15, size.y * 0.22, size.x * 0.85, size.y * 0.66); void _layoutHand(GameState s) { + // در زمانِ اینترو/بُر زدنِ ابتدای هَند، کارت‌های دست پنهان‌اند و پس از پایانِ + // بُر زدن «پخش» می‌شوند (می‌آیند). در زمانِ پخشِ بقیه‌ی کارت‌ها مخفی نمی‌شوند. + if (_introActive || _shuffling) { + for (final c in _hand.values.toList()) { + c.removeFromParent(); + } + _hand.clear(); + return; + } // مرتب‌سازی بر اساس خال و رتبه تا انتخاب برای بازیکن راحت‌تر باشد. final codes = List.from(s.yourHand)..sort(compareCards); final n = codes.length; @@ -372,6 +407,15 @@ class HokmGame extends FlameGame { } void _layoutTrick(GameState s) { + // در زمانِ اینترو/بُر زدن/پخش، کارت‌های روی میز پنهان می‌مانند تا با انیمیشن + // تداخل نکنند (پس از پایانِ انیمیشن دوباره چیده می‌شوند). + if (_introActive || _shuffling || _dealing) { + for (final c in _trick.values.toList()) { + c.removeFromParent(); + } + _trick.clear(); + return; + } final present = s.trick.map((t) => t.card).toSet(); // کارت‌های trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده برو و حذف شو. @@ -463,7 +507,10 @@ class HokmGame extends FlameGame { _backs.clear(); _info.clear(); - for (var seat = 0; seat < 4; seat++) { + // پشتِ کارتِ حریفان در زمانِ بُر زدنِ ابتدای هَند پنهان است (انیمیشنِ بُر + // خودش کارت‌ها را پخش می‌کند)؛ پس از آن نمایش داده می‌شود. + final hideBacks = _introActive || _shuffling; + for (var seat = 0; seat < 4 && !hideBacks; seat++) { if (seat == s.yourSeat) continue; final count = seat < s.handCounts.length ? s.handCounts[seat] : 0; _addBacks(_rel(seat), count); diff --git a/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart b/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart index 2042d85..a3a44c6 100644 --- a/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart +++ b/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart @@ -2,15 +2,18 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flutter/material.dart'; -/// انیمیشنِ بُر زدن: دو دسته کارتِ پشت‌رو در مرکز ریفل می‌شوند، کمی بالا/پایین -/// می‌روند (بُر) و سپس به سمت چهار بازیکن پخش می‌شوند. مدتِ کل [totalMs] است -/// تا صدای بُر زدن دقیقاً هم‌زمان با پایانِ انیمیشن قطع شود. +/// انیمیشنِ پخشِ کارت. +/// - حالتِ بُر زدن ([dealOnly]=false): دو دسته در مرکز ریفل و سپس پخش می‌شوند. +/// - حالتِ پخش ([dealOnly]=true): کارت‌ها از مرکز به سمتِ چهار بازیکن پخش می‌شوند +/// (پس از انتخابِ حکم، برای پخشِ بقیه‌ی کارت‌ها). class ShuffleAnimation extends PositionComponent { final Vector2 area; - ShuffleAnimation(this.area); + final bool dealOnly; + ShuffleAnimation(this.area, {this.dealOnly = false}); - /// مدتِ کلِ انیمیشن (مرجعِ هماهنگیِ صدا). + /// مدتِ کلِ هر حالت (مرجعِ هماهنگیِ صدا). static const int totalMs = 2000; + static const int dealMs = 1300; @override Future onLoad() async { @@ -29,24 +32,41 @@ class ShuffleAnimation extends PositionComponent { ]; for (var i = 0; i < n; i++) { - final left = i.isEven; final stack = center + Vector2((i - n / 2) * cw * 0.05, 0); - final delay = i * 0.02; // ریفلِ پلکانی + final delay = i * 0.02; + if (dealOnly) { + // فقط پخش: از یک دسته‌ی مرتب در مرکز به سمتِ بازیکن‌ها. + final card = _Back(Vector2(cw, ch)) + ..anchor = Anchor.center + ..position = stack; + add(card); + card.add(MoveToEffect( + center + dirs[i % 4], + EffectController( + duration: 0.45, curve: Curves.easeIn, startDelay: 0.15 + delay), + )); + card.add(ScaleEffect.to( + Vector2.all(0.5), + EffectController(duration: 0.45, startDelay: 0.15 + delay), + )); + continue; + } + + // حالتِ بُر زدن: ریفل سپس پخش. + final left = i.isEven; final card = _Back(Vector2(cw, ch)) ..anchor = Anchor.center ..position = center + Vector2((left ? -1 : 1) * cw, 0) ..angle = (left ? -1 : 1) * 0.22; add(card); - // صاف‌شدنِ زاویه هم‌زمان با ریفل. card.add(RotateEffect.to( 0, EffectController(duration: 0.55, startDelay: delay))); - - // دنباله: ریفل به مرکز ← بالا (بُر) ← نشستن ← پخش به سمت بازیکن. card.add(SequenceEffect([ MoveToEffect(stack, - EffectController(duration: 0.55, curve: Curves.easeOut, startDelay: delay)), + EffectController( + duration: 0.55, curve: Curves.easeOut, startDelay: delay)), MoveToEffect(stack + Vector2(0, -ch * 0.18), EffectController(duration: 0.35, curve: Curves.easeInOut)), MoveToEffect(stack, @@ -54,15 +74,13 @@ class ShuffleAnimation extends PositionComponent { MoveToEffect(center + dirs[i % 4], EffectController(duration: 0.55, curve: Curves.easeIn)), ])); - - // کوچک‌شدن هنگامِ پخش (هم‌زمان با حرکتِ آخرِ دنباله). card.add(ScaleEffect.to( Vector2.all(0.5), EffectController(duration: 0.55, startDelay: 1.25 + delay), )); } - add(RemoveEffect(delay: totalMs / 1000)); + add(RemoveEffect(delay: (dealOnly ? dealMs : totalMs) / 1000)); } }