// ویجت‌های خصوصیِ HUDِ میز (نشستِ بازیکن، حلقه‌ی نوبت، نشانِ دست‌ها، دکمه‌ها). part of 'table_hud.dart'; 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: _seatFrame(player), ), boxShadow: [ const BoxShadow(color: Colors.black54, blurRadius: 6), if (isTurn) BoxShadow( color: _seatFrame(player)[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), ), ); } }