import 'package:flutter/material.dart'; import 'package:random_avatar/random_avatar.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/theme/app_theme.dart'; import '../../domain/entities/game_entities.dart'; import '../bloc/game_state.dart'; /// اوورلیِ روی صحنه‌ی Flame: آواتارِ بازیکنان با قابِ طلایی، نام، نشانِ رتبه، /// شمارنده‌ی نوبت، تاجِ حاکم و تعدادِ کارت — به‌علاوه‌ی پنلِ امتیاز/حکم (بالا-چپ)، /// نشانگرِ اتصال و دکمه‌ی خروج (بالا-راست) و دکمه‌ی گفتگو (پایین-راست). /// همه‌ی چیدمان نسبت به اندازه‌ی صفحه است تا با کارت‌های Flame هم‌تراز بماند. class TableHud extends StatelessWidget { final GameState s; final WsStatus connection; final Map chatBubbles; final VoidCallback onExit; final VoidCallback onChat; const TableHud({ super.key, required this.s, required this.connection, required this.chatBubbles, required this.onExit, required this.onChat, }); // مکانِ آواتارِ هر جایگاهِ نسبی (۰=پایین/شما، ۱=چپ، ۲=بالا، ۳=راست). static const _spots = { 0: Alignment(0, 1.0), 1: Alignment(-1.0, -0.05), // چسبیده به لبه‌ی چپ 2: Alignment(0, -0.92), 3: Alignment(1.0, -0.05), // چسبیده به لبه‌ی راست }; int _rel(int seat) => (seat - s.yourSeat + 4) % 4; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, c) { final w = c.maxWidth; final avatarD = (w * 0.17).clamp(48.0, 84.0); return Stack( children: [ for (final p in s.players) Align( alignment: _spots[_rel(p.seat)]!, child: Padding( padding: EdgeInsets.only( bottom: _rel(p.seat) == 0 ? 2 : 0, top: _rel(p.seat) == 2 ? 6 : 0, ), child: _PlayerSeat( player: p, diameter: avatarD, isTurn: s.turn == p.seat, isHakem: s.hakem == p.seat, nameAbove: _rel(p.seat) == 2, cardCount: p.seat < s.handCounts.length ? s.handCounts[p.seat] : 0, teamTricks: (p.seat % 2) < s.tricksWon.length ? s.tricksWon[p.seat % 2] : 0, turnToken: '${s.turn}|${s.trick.length}|${s.phase}', turnSeconds: s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000 : 30, showRing: s.phase == 'playing' || s.phase == 'choose_trump', bubble: chatBubbles[p.seat], ), ), ), Positioned(top: 8, left: 8, child: SafeArea(child: _scorePanel())), Positioned( top: 8, right: 8, child: SafeArea(child: _topRight(context)), ), Positioned( bottom: 16, right: 12, child: _RoundBtn( icon: Icons.chat_bubble, color: const Color(0xFF1E5FA8), onTap: onChat, ), ), ], ); }, ); } // پنلِ امتیاز (ما/حریف) + چیپِ حکم. امتیازِ تیمِ شما = scores[yourSeat%2]. Widget _scorePanel() { final myTeam = s.yourSeat % 2; final mine = myTeam < s.scores.length ? s.scores[myTeam] : 0; final opp = (1 - myTeam) < s.scores.length ? s.scores[1 - myTeam] : 0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: _panelDeco(), child: Row( children: [ _scoreCol('ما', mine), Container( width: 1, height: 30, margin: const EdgeInsets.symmetric(horizontal: 12), color: AppColors.goldFaint, ), _scoreCol('حریف', opp), ], ), ), const SizedBox(height: 6), _hokmChip(), ], ); } Widget _scoreCol(String label, int v) => Column( children: [ Text( label, style: const TextStyle(color: AppColors.gold, fontSize: 12), ), Text( '$v', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ); Widget _hokmChip() { final (glyph, color) = suitGlyph(s.trump); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: _panelDeco(), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text( 'حکم', style: TextStyle( color: AppColors.gold, fontSize: 13, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 6), Text(glyph, style: TextStyle(color: color, fontSize: 18)), ], ), ); } Widget _topRight(BuildContext context) { final ok = connection == WsStatus.connected; return Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: _panelDeco(), child: Row( children: [ Icon( ok ? Icons.wifi : Icons.wifi_off, color: ok ? AppColors.online : Colors.redAccent, size: 16, ), const SizedBox(width: 5), Text( ok ? 'آنلاین' : 'قطع', style: TextStyle( color: ok ? AppColors.online : Colors.redAccent, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), const SizedBox(width: 8), _RoundBtn( icon: Icons.logout, color: AppColors.accent, onTap: onExit, size: 40, ), ], ); } static BoxDecoration _panelDeco() => BoxDecoration( gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF2A2018), Color(0xFF14100B)], ), borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.goldDark, width: 1.4), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)], ); } /// گرادیانِ قابِ آواتار بر اساسِ نشانِ رتبه (برنز→پادشاه). پیش‌فرض: طلایی. List _rankFrame(String tier) { switch (tier) { case 'bronze': return const [Color(0xFFCD9B6A), Color(0xFF7A4A22)]; case 'silver': return const [Color(0xFFEDF1F4), Color(0xFF98A2AD)]; case 'diamond': return const [Color(0xFF9BE7F0), Color(0xFF2E8BA0)]; case 'king': return const [Color(0xFFCE9BEA), Color(0xFF6A2FA0)]; default: // gold و ناشناخته/بات return const [Color(0xFFF3D27A), AppColors.goldDark]; } } class _PlayerSeat extends StatelessWidget { final GamePlayer player; final double diameter; final bool isTurn; final bool isHakem; final bool nameAbove; final int cardCount; final int teamTricks; // دست‌های برده‌ی تیمِ این بازیکن در این هَند (tricks_won) final String turnToken; final double turnSeconds; final bool showRing; final ChatBubble? bubble; // پیام/شکلکِ فعالِ این بازیکن (یا null) const _PlayerSeat({ required this.player, required this.diameter, required this.isTurn, required this.isHakem, required this.nameAbove, required this.cardCount, required this.teamTricks, required this.turnToken, required this.turnSeconds, required this.showRing, required this.bubble, }); @override Widget build(BuildContext context) { final ringW = diameter * 0.08; final name = Container( constraints: BoxConstraints(maxWidth: diameter * 1.7), padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1), decoration: BoxDecoration( color: const Color(0xCC14100B), borderRadius: BorderRadius.circular(10), border: Border.all( color: isTurn ? AppColors.gold : const Color(0x55B8860B), ), ), child: Text( player.bot ? '${player.name} (ربات)' : player.name, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( color: isTurn ? AppColors.gold : Colors.white, fontSize: diameter * 0.18, fontWeight: FontWeight.bold, ), ), ); final avatar = SizedBox( width: diameter, height: diameter, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ // قابِ رتبه‌ای + آواتار (رنگِ قاب بر اساسِ نشانِ رتبه‌ی بازیکن). Container( width: diameter, height: diameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: _rankFrame(player.bot ? '' : player.rankTier), ), boxShadow: [ const BoxShadow(color: Colors.black54, blurRadius: 6), if (isTurn) BoxShadow( color: _rankFrame(player.rankTier)[0] .withValues(alpha: 0.6), blurRadius: 12), ], ), padding: EdgeInsets.all(ringW), child: ClipOval( child: Container( color: const Color(0xFF1A2740), child: player.bot ? Icon( Icons.smart_toy, color: Colors.white70, size: diameter * 0.5, ) : RandomAvatar( player.avatarSeed.isEmpty ? '?' : player.avatarSeed), ), ), ), // حلقه‌ی شمارشِ نوبت (روی قاب). if (isTurn && showRing) Positioned.fill( child: _TurnRing( token: turnToken, seconds: turnSeconds, stroke: ringW * 1.1, ), ), // تاجِ حاکم بالای آواتار. if (isHakem) Positioned( top: -diameter * 0.40, child: Text('👑', style: TextStyle(fontSize: diameter * 0.4)), ), // دست‌های برده‌ی تیمِ این بازیکن در این هَند (بالا-چپ، با نشانِ تک‌خال). if (teamTricks > 0) Positioned( left: -diameter * 0.06, top: -diameter * 0.06, child: _TricksBadge(count: teamTricks, diameter: diameter), ), // حبابِ تعدادِ کارت (پایین-راست). ], ), ); // نشانِ رتبه‌ی فشرده (تصویر) کنارِ آواتار. final badge = (!player.bot && player.rankTier.isNotEmpty) ? Image.asset( 'assets/images/badge/${player.rankTier}.png', width: diameter * 0.34, height: diameter * 0.34, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ) : const SizedBox.shrink(); final nameRow = Row( mainAxisSize: MainAxisSize.min, children: [ badge, if (!player.bot && player.rankTier.isNotEmpty) const SizedBox(width: 4), Flexible(child: name), if (!player.bot && player.coins > 0) ...[ const SizedBox(width: 4), Text( '${player.coins}', style: TextStyle( color: AppColors.gold, fontSize: diameter * 0.16, ), ), ], ], ); final content = Column( mainAxisSize: MainAxisSize.min, children: nameAbove ? [nameRow, SizedBox(height: diameter * 0.06), avatar] : [avatar, SizedBox(height: diameter * 0.06), nameRow], ); if (bubble == null) return content; // حبابِ پیام بالای آواتار (برای بازیکنِ بالا، زیرِ آواتار تا از صفحه بیرون نزند). return Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ content, Positioned( top: nameAbove ? null : -diameter * 0.62, bottom: nameAbove ? -diameter * 0.62 : null, child: _ChatBubbleView(bubble: bubble!, diameter: diameter), ), ], ); } } /// نمایشِ حبابِ پیام/شکلک با یک «پاپ»ِ ورود. شکلک بزرگ و متن در یک پیلِ روشن. class _ChatBubbleView extends StatelessWidget { final ChatBubble bubble; final double diameter; const _ChatBubbleView({required this.bubble, required this.diameter}); @override Widget build(BuildContext context) { final isEmoji = (bubble.emoji ?? '').isNotEmpty; final child = isEmoji ? Text(bubble.emoji!, style: TextStyle(fontSize: diameter * 0.7)) : Container( constraints: BoxConstraints(maxWidth: diameter * 2.4), padding: EdgeInsets.symmetric( horizontal: diameter * 0.16, vertical: diameter * 0.08), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(diameter * 0.3), border: Border.all(color: AppColors.goldDark, width: 1.4), boxShadow: const [ BoxShadow(color: Colors.black54, blurRadius: 5) ], ), child: Text( bubble.text ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( color: AppColors.bgDark, fontSize: diameter * 0.2, fontWeight: FontWeight.bold, ), ), ); return TweenAnimationBuilder( key: ValueKey(bubble.id), tween: Tween(begin: 0.5, end: 1.0), duration: const Duration(milliseconds: 220), curve: Curves.easeOutBack, builder: (_, v, c) => Transform.scale(scale: v, child: c), child: child, ); } } /// حلقه‌ی شمارشِ معکوسِ نوبت: از پر به خالی طیِ `seconds`؛ نزدیکِ پایان قرمز /// می‌شود. با تغییرِ `token` (نوبتِ جدید) از نو شروع می‌شود. class _TurnRing extends StatefulWidget { final String token; final double seconds; final double stroke; const _TurnRing({ required this.token, required this.seconds, required this.stroke, }); @override State<_TurnRing> createState() => _TurnRingState(); } class _TurnRingState extends State<_TurnRing> with SingleTickerProviderStateMixin { late final AnimationController _c; @override void initState() { super.initState(); _c = AnimationController( vsync: this, duration: Duration(milliseconds: (widget.seconds * 1000).round()), )..forward(); } @override void didUpdateWidget(covariant _TurnRing old) { super.didUpdateWidget(old); if (old.token != widget.token) { _c.duration = Duration(milliseconds: (widget.seconds * 1000).round()); _c ..reset() ..forward(); } } @override void dispose() { _c.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _c, builder: (_, __) { final left = (1 - _c.value).clamp(0.0, 1.0); return CustomPaint( painter: _RingPainter(left: left, stroke: widget.stroke), ); }, ); } } class _RingPainter extends CustomPainter { final double left; // ۱..۰ final double stroke; _RingPainter({required this.left, required this.stroke}); @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; final center = rect.center; final radius = (size.shortestSide - stroke) / 2; const start = -1.5708; // بالا final sweep = 6.28318 * left; final color = left < 0.3 ? AppColors.suitRed : AppColors.online; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), start, sweep, false, Paint() ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = stroke ..color = color, ); } @override bool shouldRepaint(covariant _RingPainter old) => old.left != left || old.stroke != stroke; } /// نشانِ دست‌های برده‌ی تیم؛ هنگامِ نمایش و هر بار که عدد زیاد می‌شود یک «پاپ» /// (بزرگ‌نماییِ کوتاه) می‌زند تا کاربر بفهمد دست‌های برده اینجا نشان داده می‌شوند. class _TricksBadge extends StatefulWidget { final int count; final double diameter; const _TricksBadge({required this.count, required this.diameter}); @override State<_TricksBadge> createState() => _TricksBadgeState(); } class _TricksBadgeState extends State<_TricksBadge> with SingleTickerProviderStateMixin { late final AnimationController _c; late final Animation _scale; @override void initState() { super.initState(); _c = AnimationController( vsync: this, duration: const Duration(milliseconds: 420)); _scale = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 0.4, end: 1.35), weight: 50), TweenSequenceItem(tween: Tween(begin: 1.35, end: 1.0), weight: 50), ]).animate(CurvedAnimation(parent: _c, curve: Curves.easeOut)); _c.forward(); // پاپِ اولیه هنگامِ ظاهرشدن } @override void didUpdateWidget(covariant _TricksBadge old) { super.didUpdateWidget(old); if (old.count != widget.count) { _c ..reset() ..forward(); // پاپ هر بار که دستِ برده اضافه می‌شود } } @override void dispose() { _c.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final d = widget.diameter; return ScaleTransition( scale: _scale, child: Container( padding: EdgeInsets.symmetric(horizontal: d * 0.08, vertical: d * 0.03), decoration: BoxDecoration( color: const Color(0xFF7B0E14), borderRadius: BorderRadius.circular(d), border: Border.all(color: AppColors.gold, width: 1.4), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 4)], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.layers, color: Colors.white, size: d * 0.18), SizedBox(width: d * 0.03), Text('${widget.count}', style: TextStyle( color: Colors.white, fontSize: d * 0.2, fontWeight: FontWeight.bold)), ], ), ), ); } } class _RoundBtn extends StatelessWidget { final IconData icon; final Color color; final VoidCallback onTap; final double size; const _RoundBtn({ required this.icon, required this.color, required this.onTap, this.size = 46, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: size, height: size, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all(color: AppColors.gold, width: 1.6), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)], ), child: Icon(icon, color: Colors.white, size: size * 0.5), ), ); } }