775 lines
30 KiB
Dart
775 lines
30 KiB
Dart
// اوورلیهای خصوصیِ صفحهی بازی (بنرها، پنلِ جستجو، انتخابِ حکم، نتیجهی دست/بازی).
|
|
part of 'game_screen.dart';
|
|
|
|
/// بنرِ «نوبت شماست» وقتی کاربر باید کارت بیندازد.
|
|
class _TurnBanner extends StatelessWidget {
|
|
const _TurnBanner();
|
|
@override
|
|
Widget build(BuildContext context) => Align(
|
|
alignment: const Alignment(0, 0.3),
|
|
child: IgnorePointer(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [AppColors.gold, AppColors.goldDark],
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: const [
|
|
BoxShadow(color: Colors.black54, blurRadius: 8)
|
|
],
|
|
),
|
|
child: const Text(
|
|
'نوبت شماست',
|
|
style: TextStyle(
|
|
color: AppColors.bg,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// بنرِ قطعِ ارتباط با سرور (بالای صفحه).
|
|
class _ConnBanner extends StatelessWidget {
|
|
const _ConnBanner();
|
|
@override
|
|
Widget build(BuildContext context) => Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Material(
|
|
color: Colors.orange.shade900,
|
|
child: const SafeArea(
|
|
bottom: false,
|
|
child: Padding(
|
|
padding: EdgeInsets.all(8),
|
|
child: Text(
|
|
'ارتباط با سرور قطع شد، در حال تلاش برای اتصال مجدد…',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// نشانِ درونبازیِ تورنومنت: میفهماند این بازیِ عمومی به تورنومنت امتیاز میدهد.
|
|
class _TournamentBadge extends StatelessWidget {
|
|
final String title;
|
|
const _TournamentBadge({required this.title});
|
|
@override
|
|
Widget build(BuildContext context) => Positioned(
|
|
top: 6,
|
|
left: 0,
|
|
right: 0,
|
|
child: IgnorePointer(
|
|
child: Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF3A2A6E), AppColors.lapisInk],
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: AppColors.gold, width: 1.2),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.gold.withValues(alpha: 0.3),
|
|
blurRadius: 8),
|
|
],
|
|
),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
const Icon(Icons.emoji_events, color: AppColors.gold, size: 15),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
'تورنومنت: $title',
|
|
style: const TextStyle(
|
|
color: AppColors.gold,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// پنلِ «جستجوی حریف»: چهار جایگاه + وضعیتِ جستجو یا شمارشِ معکوسِ شروع.
|
|
class _SearchPanel extends StatelessWidget {
|
|
final GameUiState state;
|
|
final int countdown;
|
|
const _SearchPanel({required this.state, required this.countdown});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final players = state.state?.players ?? const <GamePlayer>[];
|
|
final mySeat = state.state?.yourSeat ?? -1;
|
|
final searching = state.state == null;
|
|
GamePlayer? at(int seat) {
|
|
for (final p in players) {
|
|
if (p.seat == seat) return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return Container(
|
|
color: Colors.black.withValues(alpha: 0.78),
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 18),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.panel,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: AppColors.gold, width: 2.5),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'جستجوی حریف',
|
|
style: TextStyle(
|
|
color: AppColors.gold,
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
for (var seat = 0; seat < 4; seat++)
|
|
_SearchSlot(
|
|
player: at(seat),
|
|
isYou: seat == mySeat,
|
|
seat: seat,
|
|
),
|
|
],
|
|
),
|
|
if (searching) ...[
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'در حال یافتن حریفان…',
|
|
style: TextStyle(color: Colors.white70, fontSize: 13),
|
|
),
|
|
] else if (countdown > 0) ...[
|
|
const SizedBox(height: 14),
|
|
const Text(
|
|
'شروع بازی',
|
|
style: TextStyle(color: Colors.white70, fontSize: 13),
|
|
),
|
|
const SizedBox(height: 6),
|
|
TweenAnimationBuilder<double>(
|
|
// هر عدد با یک «پاپ» ظاهر میشود تا واضح و جلبتوجه باشد.
|
|
key: ValueKey(countdown),
|
|
tween: Tween(begin: 1.6, end: 1.0),
|
|
duration: const Duration(milliseconds: 400),
|
|
curve: Curves.easeOut,
|
|
builder: (_, scale, child) =>
|
|
Transform.scale(scale: scale, child: child),
|
|
child: Container(
|
|
width: 54,
|
|
height: 54,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: const LinearGradient(
|
|
colors: [AppColors.gold, AppColors.goldDark],
|
|
),
|
|
border: Border.all(color: Colors.white, width: 2),
|
|
),
|
|
child: Text(
|
|
'$countdown',
|
|
style: const TextStyle(
|
|
color: AppColors.bg,
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// یک جایگاه در پنلِ جستجو (نام، آواتار/آیکن، برچسبِ نقش).
|
|
class _SearchSlot extends StatelessWidget {
|
|
final GamePlayer? player;
|
|
final bool isYou;
|
|
final int seat;
|
|
const _SearchSlot({
|
|
required this.player,
|
|
required this.isYou,
|
|
required this.seat,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final p = player;
|
|
final found = p != null;
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
found ? p.name : '...',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: isYou ? AppColors.gold : Colors.white70,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: 58,
|
|
height: 58,
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.bgDark,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(
|
|
color: isYou ? AppColors.gold : AppColors.goldDark,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: !found
|
|
? _SearchingAvatar(slot: seat) // آواتارهای متغیر و متفاوت
|
|
: p.bot
|
|
? const Icon(
|
|
Icons.smart_toy,
|
|
color: AppColors.gold,
|
|
size: 30,
|
|
)
|
|
: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: PlayerAvatar(
|
|
seed: p.avatarSeed,
|
|
imageUrl: p.avatarImg,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
found ? (p.bot ? 'ربات' : (isYou ? 'شما' : 'حریف')) : '',
|
|
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// دیالوگِ انتخابِ حکم توسطِ حاکم (+ بُرِ مجدد با مصرفِ یک بلیت).
|
|
class _TrumpPicker extends StatelessWidget {
|
|
const _TrumpPicker();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const suits = [
|
|
('spades', '♠', 'پیک', Colors.white),
|
|
('hearts', '♥', 'دل', AppColors.suitRed),
|
|
('diamonds', '♦', 'خشت', AppColors.suitRed),
|
|
('clubs', '♣', 'گشنیز', Colors.white),
|
|
];
|
|
return Container(
|
|
color: Colors.black54,
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
margin: const EdgeInsets.all(24),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.panel,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: AppColors.gold, width: 2),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'حکم را انتخاب کن',
|
|
style: TextStyle(color: AppColors.gold, fontSize: 20),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
for (final (id, sym, name, color) in suits)
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.bgDark,
|
|
minimumSize: const Size(120, 64),
|
|
),
|
|
onPressed: () => context.read<GameBloc>().chooseTrump(id),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(sym, style: TextStyle(fontSize: 26, color: color)),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
name,
|
|
style: const TextStyle(color: AppColors.text),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Divider(color: AppColors.goldFaint, height: 1),
|
|
const SizedBox(height: 12),
|
|
// بُرِ مجدد با مصرفِ یک بلیت (۵ کارتِ تازه، سپس باز هم انتخابِ حکم).
|
|
BlocBuilder<WalletBloc, WalletBlocState>(
|
|
builder: (context, ws) {
|
|
final tickets = ws.walletStatus is WalletLoaded
|
|
? (ws.walletStatus as WalletLoaded).wallet.tickets
|
|
: 0;
|
|
final canReshuffle = tickets > 0;
|
|
return Column(
|
|
children: [
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF8E1B7A),
|
|
disabledBackgroundColor: const Color(0xFF3A2233),
|
|
minimumSize: const Size(220, 48),
|
|
),
|
|
onPressed: canReshuffle
|
|
? () {
|
|
AppSounds.button();
|
|
context.read<GameBloc>().reshuffle();
|
|
// پس از مصرفِ بلیت، موجودی را تازه کن.
|
|
final wallet = context.read<WalletBloc>();
|
|
Future.delayed(const Duration(milliseconds: 500),
|
|
() => wallet.add(LoadWalletEvent()));
|
|
}
|
|
: null,
|
|
icon: const Icon(Icons.casino, color: Colors.white),
|
|
label: const Text(
|
|
'بُر مجدد (۱ بلیت)',
|
|
style: TextStyle(
|
|
color: Colors.white, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
canReshuffle ? 'بلیتهای شما: $tickets' : 'بلیت کافی ندارید',
|
|
style:
|
|
const TextStyle(color: Colors.white60, fontSize: 12),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// نتیجهی یک دست با بازخوردِ فوری: آیکنِ بزرگِ رنگی (جام/ضربدر)، واژهی بزرگِ
|
|
/// سبز/قرمز، واشِ رنگیِ لبههای صفحه و «پاپِ» ورودی تا کاربر بیدرنگ بفهمد بُرد یا
|
|
/// باخت. کُتِ بُرده جشنِ ویژه (کانفتیِ بیشتر + عنوانِ آتشین) دارد.
|
|
class _HandResult extends StatelessWidget {
|
|
final GameUiState state;
|
|
const _HandResult({required this.state});
|
|
|
|
// سبزِ بُرد / قرمزِ باخت (منبعِ رنگِ واش، حاشیه و هاله).
|
|
static const _winGreen = Color(0xFF3FA34D);
|
|
static const _loseRed = Color(0xFFE5484D);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final r = state.handResult!;
|
|
final myTeam = (state.state?.yourSeat ?? 0) % 2;
|
|
final won = r.winnerTeam == myTeam;
|
|
final kotWin = won && r.kot; // جشنِ ویژه فقط برای کُتِ بُرده
|
|
final accent = won ? _winGreen : _loseRed;
|
|
final mine = myTeam < r.scores.length ? r.scores[myTeam] : 0;
|
|
final opp = (1 - myTeam) < r.scores.length ? r.scores[1 - myTeam] : 0;
|
|
final title = kotWin
|
|
? (r.hakemKot ? 'حاکمکُت! 🔥' : 'کُت! 🔥')
|
|
: (won ? 'بردید!' : 'باختید');
|
|
|
|
return IgnorePointer(
|
|
child: Stack(
|
|
children: [
|
|
// ۱) واشِ رنگیِ لبهها (سبز=بُرد، قرمز=باخت) که سریع ظاهر میشود ⇒ درکِ آنی
|
|
// حتی با نگاهِ گوشهی چشم.
|
|
Positioned.fill(
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 300),
|
|
builder: (_, v, __) => DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: RadialGradient(
|
|
radius: 1.15,
|
|
colors: [
|
|
Colors.transparent,
|
|
accent.withValues(alpha: 0.30 * v),
|
|
],
|
|
stops: const [0.5, 1.0],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// ۲) کانفتی برای هر بُرد (کُت پرحجمتر).
|
|
if (won) Positioned.fill(child: Confetti(count: kotWin ? 48 : 22)),
|
|
// ۳) نشانِ مرکزی با «پاپ» ورودی.
|
|
Center(
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 500),
|
|
curve: Curves.easeOutBack,
|
|
builder: (_, v, child) => Transform.scale(
|
|
scale: v.clamp(0.0, 1.12),
|
|
child: Opacity(opacity: v.clamp(0.0, 1.0), child: child),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// آیکنِ بزرگِ رنگی: جامِ سبز برای بُرد، ضربدرِ قرمز برای باخت.
|
|
Container(
|
|
width: 96,
|
|
height: 96,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: won
|
|
? const [Color(0xFF5FD36B), Color(0xFF1B7A2E)]
|
|
: const [Color(0xFFF06A6E), Color(0xFF8E1B1F)],
|
|
),
|
|
border: Border.all(color: Colors.white, width: 3),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: accent.withValues(alpha: 0.65),
|
|
blurRadius: 26,
|
|
spreadRadius: 2),
|
|
],
|
|
),
|
|
child: Icon(
|
|
won ? Icons.emoji_events_rounded : Icons.close_rounded,
|
|
color: Colors.white,
|
|
size: 52,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
// واژهی بزرگِ نتیجه.
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
color: won ? AppColors.gold : const Color(0xFFF06A6E),
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.bold,
|
|
shadows: const [
|
|
Shadow(color: Colors.black, blurRadius: 12)
|
|
],
|
|
),
|
|
),
|
|
if (kotWin)
|
|
const Text(
|
|
'این دست را بردید',
|
|
style: TextStyle(
|
|
color: Color(0xFF7BE58A),
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
// چیپِ امتیاز/نتیجه (از دیدِ کاربر: ما · حریف).
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.55),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: accent.withValues(alpha: 0.7)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (won) ...[
|
|
Text(
|
|
'+${r.points} امتیاز',
|
|
style: const TextStyle(
|
|
color: AppColors.gold,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
Container(
|
|
width: 1,
|
|
height: 15,
|
|
margin: const EdgeInsets.symmetric(horizontal: 10),
|
|
color: Colors.white24,
|
|
),
|
|
],
|
|
Text(
|
|
'ما $mine · حریف $opp',
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// ۴) عددِ امتیاز از پنلِ امتیاز (بالا-چپ) با پرواز به نشانِ نتیجه میرسد
|
|
// تا کاربر پیوندِ امتیازِ میز با نتیجهی دست را ببیند — چه بُرد چه باخت.
|
|
_ScoreFly(points: r.points, won: won),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// عددِ امتیازِ این دست که از پنلِ امتیاز (بالا-چپِ صفحه) بهسمتِ نشانِ نتیجه
|
|
/// (مرکز) پرواز میکند و آنجا محو میشود — پیوندِ بصریِ امتیازِ میز با نتیجه.
|
|
/// بُرد: طلایی از سمتِ «ما»؛ باخت: قرمز از سمتِ «حریف».
|
|
class _ScoreFly extends StatelessWidget {
|
|
final int points;
|
|
final bool won;
|
|
const _ScoreFly({required this.points, required this.won});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// نقطهی شروع نزدیکِ عددِ مربوطه در پنلِ بالا-چپ («ما» راستتر، «حریف» چپتر).
|
|
final start =
|
|
won ? const Alignment(-0.68, -0.92) : const Alignment(-0.9, -0.92);
|
|
final grad = won
|
|
? const [Color(0xFFFFE082), AppColors.goldDark]
|
|
: const [Color(0xFFF06A6E), Color(0xFF8E1B1F)];
|
|
final textColor = won ? AppColors.bg : Colors.white;
|
|
final glow = won ? AppColors.gold : const Color(0xFFF06A6E);
|
|
|
|
return TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 850),
|
|
curve: Curves.easeInOut,
|
|
builder: (context, t, child) {
|
|
// مسیرِ حرکت: از عددِ پنل تا کمی بالای نشانِ نتیجه.
|
|
final align = Alignment.lerp(start, const Alignment(0, -0.22), t)!;
|
|
// نزدیکِ مقصد بزرگتر میشود و در انتها محو میشود (به نشان میپیوندد).
|
|
final scale = 0.85 + 0.6 * t;
|
|
final fade = t < 0.82 ? 1.0 : (1 - (t - 0.82) / 0.18);
|
|
return Align(
|
|
alignment: align,
|
|
child: Opacity(
|
|
opacity: fade.clamp(0.0, 1.0),
|
|
child: Transform.scale(scale: scale, child: child),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(colors: grad),
|
|
borderRadius: BorderRadius.circular(22),
|
|
border: Border.all(color: Colors.white, width: 1.5),
|
|
boxShadow: [
|
|
BoxShadow(color: glow.withValues(alpha: 0.75), blurRadius: 16),
|
|
],
|
|
),
|
|
child: Text(
|
|
'+$points',
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 21,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// اوورلیِ پایانِ بازی (مدال، نتیجه، پاداشها، بازگشت به لابی).
|
|
class _GameOver extends StatelessWidget {
|
|
final GameUiState state;
|
|
final int prize;
|
|
final String? tournamentTitle;
|
|
final VoidCallback onExit;
|
|
const _GameOver({
|
|
required this.state,
|
|
required this.prize,
|
|
required this.tournamentTitle,
|
|
required this.onExit,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final g = state.gameOver!;
|
|
final mySeat = state.state?.yourSeat ?? 0;
|
|
final won = g.winnerTeam == mySeat % 2;
|
|
return Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: Colors.black87)),
|
|
if (won) const Positioned.fill(child: Confetti()),
|
|
Center(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// مدالِ متحرک (تاجِ طلایی برای برد، نشانِ ملایم برای باخت).
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0, end: 1),
|
|
duration: const Duration(milliseconds: 650),
|
|
curve: Curves.easeOutBack,
|
|
builder: (_, v, __) => Transform.scale(
|
|
scale: v.clamp(0.0, 1.2),
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: won
|
|
? const [AppColors.gold, AppColors.goldDark]
|
|
: [Colors.grey.shade700, Colors.grey.shade900],
|
|
),
|
|
border: Border.all(
|
|
color: won ? Colors.white70 : Colors.white24,
|
|
width: 2),
|
|
boxShadow: won
|
|
? [
|
|
BoxShadow(
|
|
color:
|
|
AppColors.gold.withValues(alpha: 0.55),
|
|
blurRadius: 28),
|
|
]
|
|
: null,
|
|
),
|
|
child: Icon(
|
|
won
|
|
? Icons.emoji_events
|
|
: Icons.sentiment_dissatisfied_outlined,
|
|
color: won ? AppColors.lapisInk : Colors.white54,
|
|
size: 54,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
won ? 'بردید! 🎉' : 'باختید',
|
|
style: TextStyle(
|
|
color: won ? AppColors.gold : Colors.redAccent,
|
|
fontSize: 34,
|
|
fontWeight: FontWeight.bold,
|
|
shadows: const [Shadow(color: Colors.black, blurRadius: 8)],
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}',
|
|
style: const TextStyle(color: Colors.white70, fontSize: 18),
|
|
),
|
|
// پاداشِ سکهی برد (جایزهی میز).
|
|
if (won && prize > 0) ...[
|
|
const SizedBox(height: 16),
|
|
_RewardChip(
|
|
icon: Icons.monetization_on, text: '+$prize سکه'),
|
|
],
|
|
// امتیازِ کسبشدهی تورنومنت (برد ۱۰۰، شرکت ۲۵).
|
|
if (tournamentTitle != null) ...[
|
|
const SizedBox(height: 12),
|
|
_RewardChip(
|
|
icon: Icons.emoji_events,
|
|
text:
|
|
'+${won ? 100 : 25} امتیاز تورنومنت «$tournamentTitle»'),
|
|
],
|
|
const SizedBox(height: 30),
|
|
SizedBox(
|
|
width: 220,
|
|
child: ElevatedButton(
|
|
onPressed: onExit,
|
|
child: const Text('بازگشت به لابی'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// چیپِ پاداش (آیکن + متن) در اوورلیِ پایانِ بازی.
|
|
class _RewardChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String text;
|
|
const _RewardChip({required this.icon, required this.text});
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.gold.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: AppColors.gold),
|
|
),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Icon(icon, color: AppColors.gold, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(text,
|
|
style: const TextStyle(
|
|
color: AppColors.gold,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold)),
|
|
]),
|
|
);
|
|
}
|
|
|
|
/// آواتارِ در حالِ تغییر برای جایگاههای خالیِ پنل جستجو (حسِ «در حال یافتن»).
|
|
class _SearchingAvatar extends StatefulWidget {
|
|
final int slot; // هر جایگاه دامنهی seedِ متفاوت دارد تا یکسان نباشند
|
|
const _SearchingAvatar({required this.slot});
|
|
|
|
@override
|
|
State<_SearchingAvatar> createState() => _SearchingAvatarState();
|
|
}
|
|
|
|
class _SearchingAvatarState extends State<_SearchingAvatar> {
|
|
Timer? _timer;
|
|
int _i = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_i = widget.slot * 13; // شروعِ متفاوت برای هر جایگاه
|
|
_timer = Timer.periodic(const Duration(milliseconds: 240), (_) {
|
|
if (mounted) setState(() => _i++);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// پیشوندِ مخصوصِ جایگاه ⇒ آواتارِ هر جایگاه با بقیه فرق دارد.
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: RandomAvatar('s${widget.slot}-$_i'),
|
|
);
|
|
}
|
|
}
|