427 lines
14 KiB
Dart
427 lines
14 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:random_avatar/random_avatar.dart';
|
||
|
||
import '../../../../core/network/ws_client.dart';
|
||
import '../../../../core/theme/app_theme.dart';
|
||
import '../../../../core/widgets/game_ui.dart';
|
||
import '../bloc/game_bloc.dart';
|
||
import '../bloc/game_state.dart';
|
||
import 'game_screen.dart';
|
||
|
||
/// میز خصوصی: اتاق انتظار (کد، بازیکنان، شروع) سپس صحنهی بازی (روی همان اتصال).
|
||
class PrivateTableScreen extends StatelessWidget {
|
||
const PrivateTableScreen({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return BlocConsumer<GameBloc, GameUiState>(
|
||
listenWhen:
|
||
(a, b) =>
|
||
(a.notice != b.notice && b.notice != null) ||
|
||
(!a.tableClosed && b.tableClosed),
|
||
listener: (context, state) {
|
||
if (state.tableClosed) {
|
||
if (context.canPop()) context.pop();
|
||
return;
|
||
}
|
||
if (state.notice != null) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(state.notice!),
|
||
duration: const Duration(seconds: 2),
|
||
),
|
||
);
|
||
context.read<GameBloc>().clearNotice();
|
||
}
|
||
},
|
||
builder: (context, state) {
|
||
if (state.state != null) {
|
||
return const GameScreen(prize: 0);
|
||
}
|
||
return _LobbyView(state: state);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LobbyView extends StatefulWidget {
|
||
final GameUiState state;
|
||
const _LobbyView({required this.state});
|
||
|
||
@override
|
||
State<_LobbyView> createState() => _LobbyViewState();
|
||
}
|
||
|
||
class _LobbyViewState extends State<_LobbyView> {
|
||
int _hands = 3; // تعداد دستِ انتخابیِ میزبان
|
||
|
||
void _leave() {
|
||
context.read<GameBloc>().leaveTable();
|
||
if (context.canPop()) context.pop();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final state = widget.state;
|
||
final lobby = state.lobby;
|
||
final connecting = state.connection != WsStatus.connected || lobby == null;
|
||
|
||
return PopScope(
|
||
canPop: false,
|
||
onPopInvokedWithResult: (didPop, _) {
|
||
if (!didPop) _leave();
|
||
},
|
||
child: Scaffold(
|
||
body: GameBackground(
|
||
child: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
Column(
|
||
children: [
|
||
_topBar(context, lobby),
|
||
Expanded(
|
||
child:
|
||
connecting
|
||
? const Center(
|
||
child: CircularProgressIndicator(
|
||
color: AppColors.gold,
|
||
),
|
||
)
|
||
: _content(context, lobby),
|
||
),
|
||
],
|
||
),
|
||
if (state.countdown != null)
|
||
_CountdownOverlay(seconds: state.countdown!),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// نوار بالا: بازگشت (چپ) و اشتراکگذاریِ کد (راست).
|
||
Widget _topBar(BuildContext context, TableLobby? lobby) {
|
||
Widget btn(IconData icon, VoidCallback onTap) => GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: 46,
|
||
height: 46,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.panel,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: AppColors.gold, width: 1.5),
|
||
),
|
||
child: Icon(icon, color: AppColors.gold),
|
||
),
|
||
);
|
||
return Padding(
|
||
padding: const EdgeInsets.all(10),
|
||
child: Directionality(
|
||
textDirection: TextDirection.ltr,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
if (lobby != null)
|
||
btn(Icons.share, () {
|
||
Clipboard.setData(ClipboardData(text: lobby.code));
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('کد میز کپی شد؛ برای دوستانت بفرست'),
|
||
),
|
||
);
|
||
}),
|
||
btn(Icons.arrow_forward, _leave),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _content(BuildContext context, TableLobby lobby) {
|
||
final youSeat = lobby.youSeat;
|
||
// نگاشتِ جایگاهِ مطلق → بازیکن (خالی = بات هنگام شروع).
|
||
final bySeat = <int, LobbyPlayer>{for (final p in lobby.players) p.seat: p};
|
||
// چیدمان از نگاهِ خودِ کاربر: «شما» پایین، همتیمی (روبهرو) بالا، بقیه کنار.
|
||
LobbyPlayer? seatAt(int rel) => bySeat[(youSeat + rel) % 4];
|
||
final me = seatAt(0);
|
||
final top = seatAt(2); // همتیمی (روبهرو)
|
||
final left = seatAt(1);
|
||
final right = seatAt(3);
|
||
|
||
return SingleChildScrollView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Column(
|
||
children: [
|
||
const Text('شماره میز', style: TextStyle(color: Colors.white70)),
|
||
const SizedBox(height: 4),
|
||
GlowText(lobby.code, size: 36),
|
||
const SizedBox(height: 8),
|
||
// میز با چهار جایگاه.
|
||
SizedBox(
|
||
height: 340,
|
||
child: Stack(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 28,
|
||
vertical: 36,
|
||
),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: const RadialGradient(
|
||
colors: [AppColors.lapisHi, AppColors.lapisLo],
|
||
radius: 0.9,
|
||
),
|
||
borderRadius: BorderRadius.circular(160),
|
||
border: Border.all(
|
||
color: const Color(0xFF8A5A12),
|
||
width: 6,
|
||
),
|
||
),
|
||
child: const Center(child: GlowText('سلطان حکم', size: 18)),
|
||
),
|
||
),
|
||
Align(alignment: Alignment.topCenter, child: _seat(top, false)),
|
||
Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: _seat(left, false),
|
||
),
|
||
Align(
|
||
alignment: Alignment.centerRight,
|
||
child: _seat(right, false),
|
||
),
|
||
Align(
|
||
alignment: Alignment.bottomCenter,
|
||
child: _seat(me, true),
|
||
),
|
||
// چرخشِ جایگاهها (فقط میزبان) — تغییرِ تیمبندی.
|
||
if (lobby.isHost) ...[
|
||
Align(
|
||
alignment: const Alignment(-1, -0.35),
|
||
child: _rotateBtn(
|
||
Icons.rotate_left,
|
||
() => context.read<GameBloc>().rotateTable(-1),
|
||
),
|
||
),
|
||
Align(
|
||
alignment: const Alignment(1, -0.35),
|
||
child: _rotateBtn(
|
||
Icons.rotate_right,
|
||
() => context.read<GameBloc>().rotateTable(1),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text('تعداد دست', style: TextStyle(color: Colors.white70)),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
for (final h in [3, 5, 7]) _handChip(h, lobby.isHost),
|
||
],
|
||
),
|
||
const SizedBox(height: 20),
|
||
if (lobby.isHost)
|
||
GameButton(
|
||
label: 'شروع بازی',
|
||
icon: Icons.play_arrow,
|
||
width: double.infinity,
|
||
colors: const [AppColors.success, AppColors.successDark],
|
||
onTap: () => context.read<GameBloc>().startTable(_hands),
|
||
)
|
||
else
|
||
const Text(
|
||
'در انتظار شروع توسط میزبان…',
|
||
style: TextStyle(color: AppColors.gold, fontSize: 15),
|
||
),
|
||
const SizedBox(height: 6),
|
||
if (lobby.isHost)
|
||
const Text(
|
||
'جایهای خالی با ربات پر میشوند',
|
||
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||
),
|
||
const SizedBox(height: 20),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// یک جایگاهِ بازیکن دورِ میز (آواتار + ستاره + نام).
|
||
Widget _seat(LobbyPlayer? p, bool isYou) {
|
||
final empty = p == null;
|
||
return Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Stack(
|
||
clipBehavior: Clip.none,
|
||
children: [
|
||
Container(
|
||
width: 62,
|
||
height: 62,
|
||
padding: const EdgeInsets.all(3),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.bgDark,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: isYou ? AppColors.gold : AppColors.goldDark,
|
||
width: 2,
|
||
),
|
||
),
|
||
child:
|
||
empty
|
||
? const Icon(
|
||
Icons.person_add_alt_1,
|
||
color: Colors.white24,
|
||
)
|
||
: ClipRRect(
|
||
borderRadius: BorderRadius.circular(9),
|
||
child: RandomAvatar(p.name),
|
||
),
|
||
),
|
||
if (!empty)
|
||
const Positioned(
|
||
bottom: -6,
|
||
left: -4,
|
||
child: Icon(Icons.star, color: AppColors.gold, size: 22),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
empty ? 'در انتظار' : (isYou ? 'شما' : p.name),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: empty ? Colors.white38 : Colors.white,
|
||
fontSize: 12,
|
||
fontWeight: empty ? FontWeight.normal : FontWeight.bold,
|
||
),
|
||
),
|
||
if (p?.host == true)
|
||
const Text(
|
||
'میزبان',
|
||
style: TextStyle(color: AppColors.gold, fontSize: 10),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// دکمهی چرخشِ جایگاهها (کنارِ میز).
|
||
Widget _rotateBtn(IconData icon, VoidCallback onTap) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [AppColors.lapisMid, AppColors.lapisLo],
|
||
),
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(color: AppColors.gold, width: 1.5),
|
||
),
|
||
child: Icon(icon, color: AppColors.gold, size: 26),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _handChip(int h, bool editable) {
|
||
const fa = {3: '۳', 5: '۵', 7: '۷'};
|
||
final selected = _hands == h;
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
child: GestureDetector(
|
||
onTap: editable ? () => setState(() => _hands = h) : null,
|
||
child: Container(
|
||
width: 52,
|
||
height: 52,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
gradient:
|
||
selected
|
||
? const LinearGradient(
|
||
colors: [AppColors.gold, AppColors.goldDark],
|
||
)
|
||
: null,
|
||
color: selected ? null : AppColors.panel,
|
||
border: Border.all(
|
||
color: selected ? AppColors.gold : AppColors.goldDark,
|
||
width: 2,
|
||
),
|
||
),
|
||
child: Text(
|
||
fa[h]!,
|
||
style: TextStyle(
|
||
color: selected ? AppColors.bg : Colors.white,
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// اوورلی شمارش معکوس ۳، ۲، ۱ پیش از شروع بازی.
|
||
class _CountdownOverlay extends StatefulWidget {
|
||
final int seconds;
|
||
const _CountdownOverlay({required this.seconds});
|
||
|
||
@override
|
||
State<_CountdownOverlay> createState() => _CountdownOverlayState();
|
||
}
|
||
|
||
class _CountdownOverlayState extends State<_CountdownOverlay> {
|
||
late int _n;
|
||
Timer? _timer;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_n = widget.seconds;
|
||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||
if (!mounted) return;
|
||
setState(() => _n--);
|
||
if (_n <= 0) _timer?.cancel();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_timer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
color: Colors.black.withValues(alpha: 0.7),
|
||
alignment: Alignment.center,
|
||
child: TweenAnimationBuilder<double>(
|
||
key: ValueKey(_n),
|
||
tween: Tween(begin: 0.4, end: 1.2),
|
||
duration: const Duration(milliseconds: 700),
|
||
curve: Curves.easeOut,
|
||
builder:
|
||
(context, scale, child) =>
|
||
Transform.scale(scale: scale, child: child),
|
||
child: GlowText(_n > 0 ? '$_n' : 'شروع!', size: 96),
|
||
),
|
||
);
|
||
}
|
||
}
|