feat: show hand result ui

This commit is contained in:
2026-07-09 05:54:10 +03:30
parent adc2d1fc47
commit b9d21c505e
2 changed files with 194 additions and 56 deletions
@@ -97,11 +97,17 @@ class _GameScreenState extends State<GameScreen> {
(a.gameOver == null && b.gameOver != null) || (a.gameOver == null && b.gameOver != null) ||
(a.handResult == null && b.handResult != null), (a.handResult == null && b.handResult != null),
listener: (context, state) { listener: (context, state) {
// جشنِ کُت: هنگامِ دستِ کُت‌شده‌ی بُرده، ضربه‌ی لمسی. // بازخوردِ لمسیِ بُردِ دست (کُت قوی‌تر) تا کاربر بی‌درنگ بفهمد بُرد.
final hr = state.handResult; final hr = state.handResult;
if (hr != null && hr.kot) { if (hr != null) {
final myTeam = (state.state?.yourSeat ?? 0) % 2; final myTeam = (state.state?.yourSeat ?? 0) % 2;
if (hr.winnerTeam == myTeam) HapticFeedback.mediumImpact(); if (hr.winnerTeam == myTeam) {
hr.kot
? HapticFeedback.heavyImpact()
: HapticFeedback.mediumImpact();
} else {
HapticFeedback.lightImpact(); // باختِ دست هم حس شود تا پایانِ دست معلوم باشد
}
} }
if (state.gameOver != null) { if (state.gameOver != null) {
final won = final won =
@@ -181,7 +187,7 @@ class _GameScreenState extends State<GameScreen> {
}, },
), ),
if (state.handResult != null && state.gameOver == null) if (state.handResult != null && state.gameOver == null)
_HandResult(state: state), Positioned.fill(child: _HandResult(state: state)),
if (state.gameOver != null) if (state.gameOver != null)
_GameOver( _GameOver(
state: state, state: state,
@@ -376,88 +376,220 @@ class _TrumpPicker extends StatelessWidget {
} }
} }
/// نتیجه‌ی یک دست (بُرد/باخت/کُت) با جشنِ کُتِ بُرده. /// نتیجه‌ی یک دست با بازخوردِ فوری: آیکنِ بزرگِ رنگی (جام/ضربدر)، واژه‌ی بزرگِ
/// سبز/قرمز، واشِ رنگیِ لبه‌های صفحه و «پاپِ» ورودی تا کاربر بی‌درنگ بفهمد بُرد یا
/// باخت. کُتِ بُرده جشنِ ویژه (کانفتیِ بیشتر + عنوانِ آتشین) دارد.
class _HandResult extends StatelessWidget { class _HandResult extends StatelessWidget {
final GameUiState state; final GameUiState state;
const _HandResult({required this.state}); const _HandResult({required this.state});
// سبزِ بُرد / قرمزِ باخت (منبعِ رنگِ واش، حاشیه و هاله).
static const _winGreen = Color(0xFF3FA34D);
static const _loseRed = Color(0xFFE5484D);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final r = state.handResult!; final r = state.handResult!;
final mySeat = state.state?.yourSeat ?? 0; final myTeam = (state.state?.yourSeat ?? 0) % 2;
final myTeam = mySeat % 2;
final won = r.winnerTeam == myTeam; final won = r.winnerTeam == myTeam;
final kotWin = won && r.kot; // جشنِ ویژه فقط برای کُتِ بُرده 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( return IgnorePointer(
child: Stack( child: Stack(
children: [ children: [
if (kotWin) const Positioned.fill(child: Confetti(count: 40)), // ۱) واشِ رنگیِ لبه‌ها (سبز=بُرد، قرمز=باخت) که سریع ظاهر می‌شود ⇒ درکِ آنی
Center( // حتی با نگاهِ گوشه‌ی چشم.
child: Container( Positioned.fill(
padding: child: TweenAnimationBuilder<double>(
const EdgeInsets.symmetric(horizontal: 28, vertical: 18), tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 300),
builder: (_, v, __) => DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: kotWin gradient: RadialGradient(
? const LinearGradient( radius: 1.15,
colors: [AppColors.lapisMid, AppColors.lapisInk]) colors: [
: null, Colors.transparent,
color: kotWin ? null : Colors.black87, accent.withValues(alpha: 0.30 * v),
borderRadius: BorderRadius.circular(16), ],
border: stops: const [0.5, 1.0],
Border.all(color: AppColors.gold, width: kotWin ? 2 : 1), ),
boxShadow: kotWin ),
? [ ),
BoxShadow( ),
color: AppColors.gold.withValues(alpha: 0.4), ),
blurRadius: 20), // ۲) کانفتی برای هر بُرد (کُت پرحجم‌تر).
] if (won) Positioned.fill(child: Confetti(count: kotWin ? 48 : 22)),
: null, // ۳) نشانِ مرکزی با «پاپ» ورودی.
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( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (kotWin) // آیکنِ بزرگِ رنگی: جامِ سبز برای بُرد، ضربدرِ قرمز برای باخت.
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( Text(
r.hakemKot ? 'حاکم‌کُت! 🔥' : 'کُت! 🔥', 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( style: const TextStyle(
color: AppColors.gold, color: AppColors.gold,
fontSize: 30, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
shadows: [Shadow(color: Colors.black, blurRadius: 8)],
), ),
Container(
width: 1,
height: 15,
margin: const EdgeInsets.symmetric(horizontal: 10),
color: Colors.white24,
), ),
],
Text( Text(
won ? 'این دست را بردید!' : 'این دست را باختید', 'ما $mine · حریف $opp',
style: TextStyle( style: const TextStyle(
color: won ? AppColors.green : Colors.redAccent, color: Colors.white70,
fontSize: kotWin ? 16 : 20, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold, ),
],
), ),
), ),
if (r.kot && !kotWin) ],
Padding( ),
padding: const EdgeInsets.only(top: 6), ),
),
// ۴) عددِ امتیاز از پنلِ امتیاز (بالا-چپ) با پرواز به نشانِ نتیجه می‌رسد
// تا کاربر پیوندِ امتیازِ میز با نتیجه‌ی دست را ببیند — چه بُرد چه باخت.
_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( child: Text(
r.hakemKot '+$points',
? 'حاکم‌کُت! (${r.points} امتیاز)' style: TextStyle(
: 'کُت! (${r.points} امتیاز)', color: textColor,
style: const TextStyle(color: AppColors.gold), fontWeight: FontWeight.bold,
fontSize: 21,
), ),
), ),
if (kotWin)
Text('+${r.points} امتیاز',
style: const TextStyle(color: AppColors.gold)),
const SizedBox(height: 6),
Text(
'امتیاز: ${r.scores.isNotEmpty ? r.scores[0] : 0} - ${r.scores.length > 1 ? r.scores[1] : 0}',
style: const TextStyle(color: Colors.white70),
),
],
),
),
),
],
), ),
); );
} }