222 lines
7.3 KiB
Dart
222 lines
7.3 KiB
Dart
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 '../../../../core/theme/frames.dart';
|
|
import '../../../../core/theme/ranks.dart';
|
|
import '../../domain/entities/game_entities.dart';
|
|
import '../bloc/game_state.dart';
|
|
|
|
part 'table_hud.parts.dart';
|
|
|
|
/// اوورلیِ روی صحنهی Flame: آواتارِ بازیکنان با قابِ طلایی، نام، نشانِ رتبه،
|
|
/// شمارندهی نوبت، تاجِ حاکم و تعدادِ کارت — بهعلاوهی پنلِ امتیاز/حکم (بالا-چپ)،
|
|
/// نشانگرِ اتصال و دکمهی خروج (بالا-راست) و دکمهی گفتگو (پایین-راست).
|
|
/// همهی چیدمان نسبت به اندازهی صفحه است تا با کارتهای Flame همتراز بماند.
|
|
class TableHud extends StatelessWidget {
|
|
final GameState s;
|
|
final WsStatus connection;
|
|
final Map<int, ChatBubble> 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<Color> _seatFrame(GamePlayer p) =>
|
|
frameGradient(p.frame) ?? rankGradient(p.bot ? '' : p.rankTier);
|