priority of cards fixed
This commit is contained in:
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user