priority of cards fixed

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