import 'package:flutter/material.dart'; import 'package:random_avatar/random_avatar.dart'; import '../../../../core/network/ws_client.dart'; import '../../domain/entities/game_entities.dart'; /// اوورلیِ روی صحنه‌ی Flame: آواتارِ بازیکنان با قابِ طلایی، نام، نشانِ رتبه، /// شمارنده‌ی نوبت، تاجِ حاکم و تعدادِ کارت — به‌علاوه‌ی پنلِ امتیاز/حکم (بالا-چپ)، /// نشانگرِ اتصال و دکمه‌ی خروج (بالا-راست) و دکمه‌ی گفتگو (پایین-راست). /// همه‌ی چیدمان نسبت به اندازه‌ی صفحه است تا با کارت‌های Flame هم‌تراز بماند. class TableHud extends StatelessWidget { final GameState s; final WsStatus connection; final VoidCallback onExit; final VoidCallback onChat; const TableHud({ super.key, required this.s, required this.connection, required this.onExit, required this.onChat, }); // مکانِ آواتارِ هر جایگاهِ نسبی (۰=پایین/شما، ۱=چپ، ۲=بالا، ۳=راست). static const _spots = { 0: Alignment(0, 1.0), 1: Alignment(-0.94, -0.08), 2: Alignment(0, -0.92), 3: Alignment(0.94, -0.08), }; 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, turnToken: '${s.turn}|${s.trick.length}|${s.phase}', turnSeconds: s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000 : 30, showRing: s.phase == 'playing' || s.phase == 'choose_trump', ), ), ), 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: const Color(0x33E9B949), ), _scoreCol('حریف', opp), ], ), ), const SizedBox(height: 6), _hokmChip(), ], ); } Widget _scoreCol(String label, int v) => Column( children: [ Text(label, style: const TextStyle(color: Color(0xFFE9B949), fontSize: 12)), Text('$v', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), ], ); Widget _hokmChip() { final (glyph, color) = _trumpGlyph(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: Color(0xFFE9B949), 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 ? const Color(0xFF4CD964) : Colors.redAccent, size: 16), const SizedBox(width: 5), Text(ok ? 'آنلاین' : 'قطع', style: TextStyle( color: ok ? const Color(0xFF4CD964) : Colors.redAccent, fontSize: 12, fontWeight: FontWeight.bold)), ], ), ), const SizedBox(width: 8), _RoundBtn( icon: Icons.logout, color: const Color(0xFFB81D2A), 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: const Color(0xFFB8860B), width: 1.4), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)], ); (String, Color) _trumpGlyph(String? suit) { switch (suit) { case 'hearts': return ('♥', const Color(0xFFE53935)); case 'diamonds': return ('♦', const Color(0xFFE53935)); case 'clubs': return ('♣', Colors.white); case 'spades': return ('♠', Colors.white); default: return ('—', Colors.white54); } } } class _PlayerSeat extends StatelessWidget { final GamePlayer player; final double diameter; final bool isTurn; final bool isHakem; final bool nameAbove; final int cardCount; final String turnToken; final double turnSeconds; final bool showRing; const _PlayerSeat({ required this.player, required this.diameter, required this.isTurn, required this.isHakem, required this.nameAbove, required this.cardCount, required this.turnToken, required this.turnSeconds, required this.showRing, }); @override Widget build(BuildContext context) { final ringW = diameter * 0.08; final name = Container( constraints: BoxConstraints(maxWidth: diameter * 1.7), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: const Color(0xCC14100B), borderRadius: BorderRadius.circular(10), border: Border.all( color: isTurn ? const Color(0xFFE9B949) : const Color(0x55B8860B)), ), child: Text( player.bot ? '${player.name} (ربات)' : player.name, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( color: isTurn ? const Color(0xFFE9B949) : 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: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFF3D27A), Color(0xFFB8860B)], ), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)], ), 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.name.isEmpty ? '?' : player.name), ), ), ), // حلقه‌ی شمارشِ نوبت (روی قاب). if (isTurn && showRing) Positioned.fill( child: _TurnRing( token: turnToken, seconds: turnSeconds, stroke: ringW * 1.1, ), ), // تاجِ حاکم بالای آواتار. if (isHakem) Positioned( top: -diameter * 0.32, child: Icon(Icons.workspace_premium, color: const Color(0xFFFFC107), size: diameter * 0.42), ), // حبابِ تعدادِ کارت (پایین-راست). if (cardCount > 0) Positioned( right: -diameter * 0.04, bottom: -diameter * 0.04, child: Container( padding: EdgeInsets.all(diameter * 0.06), constraints: BoxConstraints( minWidth: diameter * 0.34, minHeight: diameter * 0.34), decoration: BoxDecoration( color: const Color(0xFF0E2A12), shape: BoxShape.circle, border: Border.all(color: const Color(0xFFE9B949), width: 1.4), ), alignment: Alignment.center, child: Text('$cardCount', style: TextStyle( color: Colors.white, fontSize: diameter * 0.18, fontWeight: FontWeight.bold)), ), ), ], ), ); // نشانِ رتبه‌ی فشرده (تصویر) کنارِ آواتار. 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: const Color(0xFFE9B949), fontSize: diameter * 0.16)), ], ], ); return Column( mainAxisSize: MainAxisSize.min, children: nameAbove ? [nameRow, SizedBox(height: diameter * 0.06), avatar] : [avatar, SizedBox(height: diameter * 0.06), nameRow], ); } } /// حلقه‌ی شمارشِ معکوسِ نوبت: از پر به خالی طیِ `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 ? const Color(0xFFE53935) : const Color(0xFF4CD964); 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 _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: const Color(0xFFE9B949), width: 1.6), boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)], ), child: Icon(icon, color: Colors.white, size: size * 0.5), ), ); } }