diff --git a/lib/feature/game/presentation/screen/game_screen.dart b/lib/feature/game/presentation/screen/game_screen.dart index f9aa194..9ff281b 100644 --- a/lib/feature/game/presentation/screen/game_screen.dart +++ b/lib/feature/game/presentation/screen/game_screen.dart @@ -97,11 +97,17 @@ class _GameScreenState extends State { (a.gameOver == null && b.gameOver != null) || (a.handResult == null && b.handResult != null), listener: (context, state) { - // جشنِ کُت: هنگامِ دستِ کُت‌شده‌ی بُرده، ضربه‌ی لمسی. + // بازخوردِ لمسیِ بُردِ دست (کُت قوی‌تر) تا کاربر بی‌درنگ بفهمد بُرد. final hr = state.handResult; - if (hr != null && hr.kot) { + if (hr != null) { 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) { final won = @@ -181,7 +187,7 @@ class _GameScreenState extends State { }, ), if (state.handResult != null && state.gameOver == null) - _HandResult(state: state), + Positioned.fill(child: _HandResult(state: state)), if (state.gameOver != null) _GameOver( state: state, diff --git a/lib/feature/game/presentation/screen/game_screen.parts.dart b/lib/feature/game/presentation/screen/game_screen.parts.dart index a2be3d1..a1b881a 100644 --- a/lib/feature/game/presentation/screen/game_screen.parts.dart +++ b/lib/feature/game/presentation/screen/game_screen.parts.dart @@ -376,93 +376,225 @@ class _TrumpPicker extends StatelessWidget { } } -/// نتیجه‌ی یک دست (بُرد/باخت/کُت) با جشنِ کُتِ بُرده. +/// نتیجه‌ی یک دست با بازخوردِ فوری: آیکنِ بزرگِ رنگی (جام/ضربدر)، واژه‌ی بزرگِ +/// سبز/قرمز، واشِ رنگیِ لبه‌های صفحه و «پاپِ» ورودی تا کاربر بی‌درنگ بفهمد بُرد یا +/// باخت. کُتِ بُرده جشنِ ویژه (کانفتیِ بیشتر + عنوانِ آتشین) دارد. 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 mySeat = state.state?.yourSeat ?? 0; - final myTeam = mySeat % 2; + 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: [ - if (kotWin) const Positioned.fill(child: Confetti(count: 40)), + // ۱) واشِ رنگیِ لبه‌ها (سبز=بُرد، قرمز=باخت) که سریع ظاهر می‌شود ⇒ درکِ آنی + // حتی با نگاهِ گوشه‌ی چشم. + Positioned.fill( + child: TweenAnimationBuilder( + 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: Container( - padding: - const EdgeInsets.symmetric(horizontal: 28, vertical: 18), - decoration: BoxDecoration( - gradient: kotWin - ? const LinearGradient( - colors: [AppColors.lapisMid, AppColors.lapisInk]) - : null, - color: kotWin ? null : Colors.black87, - borderRadius: BorderRadius.circular(16), - border: - Border.all(color: AppColors.gold, width: kotWin ? 2 : 1), - boxShadow: kotWin - ? [ - BoxShadow( - color: AppColors.gold.withValues(alpha: 0.4), - blurRadius: 20), - ] - : null, + child: TweenAnimationBuilder( + 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: [ - if (kotWin) - Text( - r.hakemKot ? 'حاکم‌کُت! 🔥' : 'کُت! 🔥', - style: const TextStyle( - color: AppColors.gold, - fontSize: 30, - fontWeight: FontWeight.bold, - shadows: [Shadow(color: Colors.black, blurRadius: 8)], + // آیکنِ بزرگِ رنگی: جامِ سبز برای بُرد، ضربدرِ قرمز برای باخت. + 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), + ], ), - Text( - won ? 'این دست را بردید!' : 'این دست را باختید', - style: TextStyle( - color: won ? AppColors.green : Colors.redAccent, - fontSize: kotWin ? 16 : 20, - fontWeight: FontWeight.bold, + child: Icon( + won ? Icons.emoji_events_rounded : Icons.close_rounded, + color: Colors.white, + size: 52, ), ), - if (r.kot && !kotWin) - Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - r.hakemKot - ? 'حاکم‌کُت! (${r.points} امتیاز)' - : 'کُت! (${r.points} امتیاز)', - style: const TextStyle(color: AppColors.gold), + 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, ), ), - 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), + 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( + 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;