feat: add heart beat for timing
This commit is contained in:
Binary file not shown.
@@ -15,6 +15,7 @@ class AppSounds {
|
|||||||
static const _win = 'win.mp3'; // برد بازی
|
static const _win = 'win.mp3'; // برد بازی
|
||||||
static const _button = 'button.mp3'; // فشردنِ دکمهها
|
static const _button = 'button.mp3'; // فشردنِ دکمهها
|
||||||
static const _selectHokm = 'select_hokm.mp3'; // انتخابِ حکم
|
static const _selectHokm = 'select_hokm.mp3'; // انتخابِ حکم
|
||||||
|
static const _heartBeat = 'heart_beat.mp3'; // ثانیههای پایانیِ نوبت
|
||||||
|
|
||||||
static const _all = [
|
static const _all = [
|
||||||
_shuffle,
|
_shuffle,
|
||||||
@@ -24,6 +25,7 @@ class AppSounds {
|
|||||||
_win,
|
_win,
|
||||||
_button,
|
_button,
|
||||||
_selectHokm,
|
_selectHokm,
|
||||||
|
_heartBeat,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final Map<String, AudioPool> _pools = {};
|
static final Map<String, AudioPool> _pools = {};
|
||||||
@@ -57,5 +59,26 @@ class AppSounds {
|
|||||||
static void cut() => _play(_cut, 0.9);
|
static void cut() => _play(_cut, 0.9);
|
||||||
static void win() => _play(_win, 0.9);
|
static void win() => _play(_win, 0.9);
|
||||||
static void selectHokm() => _play(_selectHokm, 0.9);
|
static void selectHokm() => _play(_selectHokm, 0.9);
|
||||||
|
|
||||||
|
// کنترلِ ضربانِ قلب: همیشه حداکثر یک ضربان همزمان (بدون انباشت/تکرارِ سریع)
|
||||||
|
// و قابلِ توقف وقتی نوبت تمام میشود.
|
||||||
|
static Function? _hbStop;
|
||||||
|
|
||||||
|
static void heartBeat() {
|
||||||
|
final pool = _pools[_heartBeat];
|
||||||
|
if (muted || pool == null) return;
|
||||||
|
_hbStop?.call(); // قطعِ ضربانِ قبلی پیش از پخشِ جدید
|
||||||
|
_hbStop = null;
|
||||||
|
pool.start(volume: 0.8).then((stop) {
|
||||||
|
_hbStop = stop;
|
||||||
|
}, onError: (_) {});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stopHeartBeat() {
|
||||||
|
try {
|
||||||
|
_hbStop?.call();
|
||||||
|
} catch (_) {}
|
||||||
|
_hbStop = null;
|
||||||
|
}
|
||||||
static void button() => _play(_button, 0.5, cutoffMs: 130); // کلیکِ کوتاه
|
static void button() => _play(_button, 0.5, cutoffMs: 130); // کلیکِ کوتاه
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flame/game.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:random_avatar/random_avatar.dart';
|
||||||
|
|
||||||
import '../../../../core/network/ws_client.dart';
|
import '../../../../core/network/ws_client.dart';
|
||||||
import '../../../../core/service/app_sounds.dart';
|
import '../../../../core/service/app_sounds.dart';
|
||||||
@@ -50,14 +51,18 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
if (!didPop) _confirmLeave(context);
|
if (!didPop) _confirmLeave(context);
|
||||||
},
|
},
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: BlocConsumer<GameBloc, GameUiState>(
|
body: BlocConsumer<GameBloc, GameUiState>(
|
||||||
listenWhen: (a, b) =>
|
listenWhen:
|
||||||
|
(a, b) =>
|
||||||
(a.notice != b.notice && b.notice != null) ||
|
(a.notice != b.notice && b.notice != null) ||
|
||||||
(a.gameOver == null && b.gameOver != null),
|
(a.gameOver == null && b.gameOver != null),
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.gameOver != null) {
|
if (state.gameOver != null) {
|
||||||
final won = state.gameOver!.winnerTeam ==
|
final won =
|
||||||
|
state.gameOver!.winnerTeam ==
|
||||||
(state.state?.yourSeat ?? 0) % 2;
|
(state.state?.yourSeat ?? 0) % 2;
|
||||||
if (won) AppSounds.win();
|
if (won) AppSounds.win();
|
||||||
}
|
}
|
||||||
@@ -65,7 +70,8 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(state.notice!),
|
content: Text(state.notice!),
|
||||||
duration: const Duration(seconds: 2)),
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
context.read<GameBloc>().clearNotice();
|
context.read<GameBloc>().clearNotice();
|
||||||
// خطا پیش از شروعِ بازی (مثلاً سکهی ناکافی) ⇒ کاربر در دیالوگِ
|
// خطا پیش از شروعِ بازی (مثلاً سکهی ناکافی) ⇒ کاربر در دیالوگِ
|
||||||
@@ -95,13 +101,15 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
_trumpPicker(context),
|
_trumpPicker(context),
|
||||||
if (state.handResult != null && state.gameOver == null)
|
if (state.handResult != null && state.gameOver == null)
|
||||||
_handResult(state),
|
_handResult(state),
|
||||||
if (!_showSearch(state) && _isMyPlayTurn(state)) _turnBanner(),
|
if (!_showSearch(state) && _isMyPlayTurn(state))
|
||||||
|
_turnBanner(),
|
||||||
if (state.gameOver != null) _gameOver(context, state),
|
if (state.gameOver != null) _gameOver(context, state),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,21 +122,25 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
|
|
||||||
// بنرِ «نوبت شماست» وقتی کاربر باید کارت بیندازد.
|
// بنرِ «نوبت شماست» وقتی کاربر باید کارت بیندازد.
|
||||||
Widget _turnBanner() => Align(
|
Widget _turnBanner() => Align(
|
||||||
alignment: const Alignment(0, 0.46),
|
alignment: const Alignment(0, 0.80),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [Color(0xFFE9B949), Color(0xFFB8860B)]),
|
colors: [Color(0xFFE9B949), Color(0xFFB8860B)],
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 8)],
|
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 8)],
|
||||||
),
|
),
|
||||||
child: const Text('نوبت شماست',
|
child: const Text(
|
||||||
|
'نوبت شماست',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF3A0A12),
|
color: Color(0xFF3A0A12),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16)),
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -157,17 +169,22 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
}
|
}
|
||||||
final yes = await showDialog<bool>(
|
final yes = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => AlertDialog(
|
builder:
|
||||||
|
(_) => AlertDialog(
|
||||||
backgroundColor: AppColors.panel,
|
backgroundColor: AppColors.panel,
|
||||||
title: const Text('خروج از میز'),
|
title: const Text('خروج از میز'),
|
||||||
content: const Text('از میز خارج میشوید؟ ورودی بازگردانده نمیشود.'),
|
content: const Text(
|
||||||
|
'از میز خارج میشوید؟ ورودی بازگردانده نمیشود.',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
onPressed: () => Navigator.pop(context, false),
|
||||||
child: const Text('ماندن')),
|
child: const Text('ماندن'),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, true),
|
onPressed: () => Navigator.pop(context, true),
|
||||||
child: const Text('خروج')),
|
child: const Text('خروج'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -193,9 +210,11 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
child: Text('ارتباط با سرور قطع شد، در حال تلاش برای اتصال مجدد…',
|
child: Text(
|
||||||
|
'ارتباط با سرور قطع شد، در حال تلاش برای اتصال مجدد…',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.white)),
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -226,11 +245,14 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text('جستجوی حریف',
|
const Text(
|
||||||
|
'جستجوی حریف',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.gold,
|
color: AppColors.gold,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -239,31 +261,11 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
_searchSlot(at(seat), seat == mySeat),
|
_searchSlot(at(seat), seat == mySeat),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.bgDark,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: AppColors.goldDark),
|
|
||||||
),
|
|
||||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
||||||
const Icon(Icons.monetization_on, color: AppColors.gold),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text('${widget.prize}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.gold,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
if (searching) ...[
|
if (searching) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(
|
const Text(
|
||||||
height: 22,
|
'در حال یافتن حریفان…',
|
||||||
width: 22,
|
style: TextStyle(color: Colors.white70, fontSize: 13),
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2, color: AppColors.gold),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -276,34 +278,52 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
final found = p != null;
|
final found = p != null;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
child: Column(
|
||||||
Text(found ? p.name : '...',
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
found ? p.name : '...',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isYou ? AppColors.gold : Colors.white70, fontSize: 12)),
|
color: isYou ? AppColors.gold : Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Container(
|
||||||
width: 58,
|
width: 58,
|
||||||
height: 58,
|
height: 58,
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.bgDark,
|
color: AppColors.bgDark,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isYou ? AppColors.gold : AppColors.goldDark, width: 1.5),
|
color: isYou ? AppColors.gold : AppColors.goldDark,
|
||||||
|
width: 1.5,
|
||||||
),
|
),
|
||||||
child: Icon(
|
),
|
||||||
found
|
child:
|
||||||
? (p.bot ? Icons.smart_toy : Icons.person)
|
!found
|
||||||
: Icons.help_outline,
|
? const _SearchingAvatar() // آواتارهای متغیر تا یافتنِ حریف
|
||||||
color: found ? AppColors.gold : Colors.white24,
|
: p.bot
|
||||||
|
? const Icon(
|
||||||
|
Icons.smart_toy,
|
||||||
|
color: AppColors.gold,
|
||||||
size: 30,
|
size: 30,
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: RandomAvatar(p.name),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(found ? (p.bot ? 'ربات' : (isYou ? 'شما' : 'حریف')) : '',
|
Text(
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 10)),
|
found ? (p.bot ? 'ربات' : (isYou ? 'شما' : 'حریف')) : '',
|
||||||
]),
|
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,8 +348,10 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text('حکم را انتخاب کن',
|
const Text(
|
||||||
style: TextStyle(color: AppColors.gold, fontSize: 20)),
|
'حکم را انتخاب کن',
|
||||||
|
style: TextStyle(color: AppColors.gold, fontSize: 20),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
@@ -343,11 +365,17 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
minimumSize: const Size(120, 64),
|
minimumSize: const Size(120, 64),
|
||||||
),
|
),
|
||||||
onPressed: () => context.read<GameBloc>().chooseTrump(id),
|
onPressed: () => context.read<GameBloc>().chooseTrump(id),
|
||||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
Text(sym, style: TextStyle(fontSize: 26, color: color)),
|
Text(sym, style: TextStyle(fontSize: 26, color: color)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(name, style: const TextStyle(color: AppColors.text)),
|
Text(
|
||||||
]),
|
name,
|
||||||
|
style: const TextStyle(color: AppColors.text),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -371,12 +399,17 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: AppColors.gold),
|
border: Border.all(color: AppColors.gold),
|
||||||
),
|
),
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
child: Column(
|
||||||
Text(won ? 'این دست را بردید!' : 'این دست را باختید',
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
won ? 'این دست را بردید!' : 'این دست را باختید',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: won ? AppColors.green : Colors.redAccent,
|
color: won ? AppColors.green : Colors.redAccent,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (r.kot)
|
if (r.kot)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
@@ -384,13 +417,16 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
r.hakemKot
|
r.hakemKot
|
||||||
? 'حاکمکُت! (${r.points} امتیاز)'
|
? 'حاکمکُت! (${r.points} امتیاز)'
|
||||||
: 'کُت! (${r.points} امتیاز)',
|
: 'کُت! (${r.points} امتیاز)',
|
||||||
style: const TextStyle(color: AppColors.gold)),
|
style: const TextStyle(color: AppColors.gold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'امتیاز: ${r.scores.isNotEmpty ? r.scores[0] : 0} - ${r.scores.length > 1 ? r.scores[1] : 0}',
|
'امتیاز: ${r.scores.isNotEmpty ? r.scores[0] : 0} - ${r.scores.length > 1 ? r.scores[1] : 0}',
|
||||||
style: const TextStyle(color: Colors.white70)),
|
style: const TextStyle(color: Colors.white70),
|
||||||
]),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -403,16 +439,22 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
return Container(
|
return Container(
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
child: Column(
|
||||||
Text(won ? 'بردید! 🎉' : 'باختید',
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
won ? 'بردید! 🎉' : 'باختید',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: won ? AppColors.gold : Colors.redAccent,
|
color: won ? AppColors.gold : Colors.redAccent,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}',
|
'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}',
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 18)),
|
style: const TextStyle(color: Colors.white70, fontSize: 18),
|
||||||
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 220,
|
width: 220,
|
||||||
@@ -421,7 +463,43 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
child: const Text('بازگشت به لابی'),
|
child: const Text('بازگشت به لابی'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// آواتارِ در حالِ تغییر برای جایگاههای خالیِ پنل جستجو (حسِ «در حال یافتن»).
|
||||||
|
class _SearchingAvatar extends StatefulWidget {
|
||||||
|
const _SearchingAvatar();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SearchingAvatar> createState() => _SearchingAvatarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchingAvatarState extends State<_SearchingAvatar> {
|
||||||
|
Timer? _timer;
|
||||||
|
int _i = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 220), (_) {
|
||||||
|
if (mounted) setState(() => _i++);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: RandomAvatar('seek-${_i % 24}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,13 +42,28 @@ class CardComponent extends PositionComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// بزرگنماییِ کارتِ انتخابشده هنگام کشیدن (واضحتر برای دیدن).
|
||||||
|
static const _dragScale = 1.4;
|
||||||
|
|
||||||
|
void _scaleTo(double s) {
|
||||||
|
children.whereType<ScaleEffect>().toList().forEach((e) => e.removeFromParent());
|
||||||
|
add(ScaleEffect.to(
|
||||||
|
Vector2.all(s), EffectController(duration: 0.12, curve: Curves.easeOut)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// بازنشانیِ فوریِ مقیاس به اندازهی عادی (هنگام بازی روی میز).
|
||||||
|
void resetScale() {
|
||||||
|
children.whereType<ScaleEffect>().toList().forEach((e) => e.removeFromParent());
|
||||||
|
scale = Vector2.all(1);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onDragStart(DragStartEvent event) {
|
void onDragStart(DragStartEvent event) {
|
||||||
super.onDragStart(event);
|
super.onDragStart(event);
|
||||||
if (!_playable) return;
|
if (!_playable) return;
|
||||||
_dragging = true;
|
_dragging = true;
|
||||||
priority = 1000; // روی همهی کارتها
|
priority = 1000; // روی همهی کارتها
|
||||||
scale = Vector2.all(1.12); // بزرگنمایی هنگام کشیدن (فیدبک)
|
_scaleTo(_dragScale); // بزرگنماییِ نرم هنگام انتخاب
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -63,11 +78,13 @@ class CardComponent extends PositionComponent
|
|||||||
super.onDragEnd(event);
|
super.onDragEnd(event);
|
||||||
if (!_dragging) return;
|
if (!_dragging) return;
|
||||||
_dragging = false;
|
_dragging = false;
|
||||||
scale = Vector2.all(1);
|
// فقط اگر داخل ناحیهی وسط میز رها شد ⇒ بازی (اندازه به حالت عادی)؛
|
||||||
// فقط اگر داخل ناحیهی وسط میز رها شد ⇒ بازی؛ وگرنه برگشت به جای مرتبِ خود.
|
// وگرنه برگشت به جای مرتبِ خود.
|
||||||
if (_inDropZone()) {
|
if (_inDropZone()) {
|
||||||
|
resetScale();
|
||||||
onPlay?.call();
|
onPlay?.call();
|
||||||
} else {
|
} else {
|
||||||
|
_scaleTo(1);
|
||||||
_returnHome();
|
_returnHome();
|
||||||
}
|
}
|
||||||
_overZone = false;
|
_overZone = false;
|
||||||
@@ -79,7 +96,7 @@ class CardComponent extends PositionComponent
|
|||||||
if (!_dragging) return;
|
if (!_dragging) return;
|
||||||
_dragging = false;
|
_dragging = false;
|
||||||
_overZone = false;
|
_overZone = false;
|
||||||
scale = Vector2.all(1);
|
_scaleTo(1);
|
||||||
_returnHome();
|
_returnHome();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,15 +164,15 @@ class CardComponent extends PositionComponent
|
|||||||
..color = const Color(0xFF1A1A1A),
|
..color = const Color(0xFF1A1A1A),
|
||||||
);
|
);
|
||||||
|
|
||||||
// هایلایت طلایی وقتی کارت روی ناحیهی انداختن است (رهاکنی، بازی میشود).
|
// هایلایت طلایی: هنگام کشیدن همیشه (انتخاب)، و پررنگتر روی ناحیهی انداختن.
|
||||||
if (_dragging && _overZone) {
|
if (_dragging) {
|
||||||
canvas.drawRRect(
|
canvas.drawRRect(
|
||||||
rrect,
|
rrect,
|
||||||
Paint()
|
Paint()
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = size.x * 0.06
|
..strokeWidth = size.x * (_overZone ? 0.06 : 0.035)
|
||||||
..color = const Color(0xFFE9B949)
|
..color = const Color(0xFFE9B949)
|
||||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6),
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, _overZone ? 6 : 3),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,16 @@ class HokmGame extends FlameGame {
|
|||||||
int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن
|
int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن
|
||||||
bool _silent = false; // هنگام خروج از میز، دیگر صدایی پخش نشود
|
bool _silent = false; // هنگام خروج از میز، دیگر صدایی پخش نشود
|
||||||
bool _introActive = true; // تا پایانِ نمایشِ «جستجوی حریف»
|
bool _introActive = true; // تا پایانِ نمایشِ «جستجوی حریف»
|
||||||
bool _pendingShuffle = false; // بُرِ دستِ اول که تا پایانِ اینترو به تعویق افتاده
|
bool _pendingShuffle =
|
||||||
|
false; // بُرِ دستِ اول که تا پایانِ اینترو به تعویق افتاده
|
||||||
bool _shuffling = false; // در حالِ پخشِ انیمیشنِ بُر زدن (بازیِ کارت ممنوع)
|
bool _shuffling = false; // در حالِ پخشِ انیمیشنِ بُر زدن (بازیِ کارت ممنوع)
|
||||||
String? _prevTrump; // برای تشخیصِ لحظهی انتخابِ حکم
|
String? _prevTrump; // برای تشخیصِ لحظهی انتخابِ حکم
|
||||||
|
|
||||||
|
// مدیریتِ ضربانِ قلب (فقط نوبتِ خودِ کاربر؛ مستقل از بازساختِ صحنه).
|
||||||
|
double _turnElapsed = 0;
|
||||||
|
bool _beatPlayed = false; // یکبار در هر نوبت، هنگام رسیدن به آستانه
|
||||||
|
bool _wasMyTurn = false;
|
||||||
|
|
||||||
HokmGame(this.cubit);
|
HokmGame(this.cubit);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -101,6 +107,7 @@ class HokmGame extends FlameGame {
|
|||||||
@override
|
@override
|
||||||
void onRemove() {
|
void onRemove() {
|
||||||
_sub?.cancel();
|
_sub?.cancel();
|
||||||
|
AppSounds.stopHeartBeat();
|
||||||
super.onRemove();
|
super.onRemove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +125,8 @@ class HokmGame extends FlameGame {
|
|||||||
now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now);
|
now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now);
|
||||||
if (addedOne) {
|
if (addedOne) {
|
||||||
final card = s.trick.last;
|
final card = s.trick.last;
|
||||||
final isCut = s.trump != null &&
|
final isCut =
|
||||||
|
s.trump != null &&
|
||||||
s.leadSuit != null &&
|
s.leadSuit != null &&
|
||||||
s.leadSuit != s.trump &&
|
s.leadSuit != s.trump &&
|
||||||
suitName(card.card) == s.trump;
|
suitName(card.card) == s.trump;
|
||||||
@@ -136,7 +144,10 @@ class HokmGame extends FlameGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// قطعِ صداهای بازی هنگام خروج از میز (جلوگیری از پخشِ صدای کارت پس از ترک).
|
/// قطعِ صداهای بازی هنگام خروج از میز (جلوگیری از پخشِ صدای کارت پس از ترک).
|
||||||
void silence() => _silent = true;
|
void silence() {
|
||||||
|
_silent = true;
|
||||||
|
AppSounds.stopHeartBeat();
|
||||||
|
}
|
||||||
|
|
||||||
/// پایانِ نمایشِ «جستجوی حریف»؛ اگر بُرِ دستِ اول به تعویق افتاده بود، اکنون
|
/// پایانِ نمایشِ «جستجوی حریف»؛ اگر بُرِ دستِ اول به تعویق افتاده بود، اکنون
|
||||||
/// (که اوورلی کنار رفته) با صدا و تصویرِ هماهنگ اجرا میشود.
|
/// (که اوورلی کنار رفته) با صدا و تصویرِ هماهنگ اجرا میشود.
|
||||||
@@ -180,8 +191,12 @@ class HokmGame extends FlameGame {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
reveal.add(ScaleEffect.to(
|
reveal.add(
|
||||||
Vector2.all(1.0), EffectController(duration: 0.35, curve: Curves.easeOutBack)));
|
ScaleEffect.to(
|
||||||
|
Vector2.all(1.0),
|
||||||
|
EffectController(duration: 0.35, curve: Curves.easeOutBack),
|
||||||
|
),
|
||||||
|
);
|
||||||
reveal.add(RemoveEffect(delay: 1.3));
|
reveal.add(RemoveEffect(delay: 1.3));
|
||||||
add(reveal);
|
add(reveal);
|
||||||
}
|
}
|
||||||
@@ -198,6 +213,44 @@ class HokmGame extends FlameGame {
|
|||||||
super.update(dt);
|
super.update(dt);
|
||||||
_t += dt;
|
_t += dt;
|
||||||
if (_shake > 0) _shake = math.max(0, _shake - dt * 2.2);
|
if (_shake > 0) _shake = math.max(0, _shake - dt * 2.2);
|
||||||
|
_updateHeartBeat(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ضربانِ قلب فقط در ثانیههای پایانیِ نوبتِ خودِ کاربر؛ هنگامِ پایانِ نوبت
|
||||||
|
// (بازیشدنِ کارت توسط کاربر یا سیستم) بلافاصله قطع میشود.
|
||||||
|
void _updateHeartBeat(double dt) {
|
||||||
|
final s = _s;
|
||||||
|
final myTurn = !_silent &&
|
||||||
|
!_introActive &&
|
||||||
|
!_shuffling &&
|
||||||
|
s != null &&
|
||||||
|
((s.phase == 'playing' && s.turn == s.yourSeat) ||
|
||||||
|
(s.phase == 'choose_trump' && s.hakem == s.yourSeat));
|
||||||
|
|
||||||
|
if (!myTurn) {
|
||||||
|
if (_wasMyTurn) {
|
||||||
|
AppSounds.stopHeartBeat(); // نوبت تمام شد ⇒ توقفِ فوریِ صدا
|
||||||
|
_wasMyTurn = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_wasMyTurn) {
|
||||||
|
// شروعِ نوبتِ من ⇒ شمارش از صفر.
|
||||||
|
_wasMyTurn = true;
|
||||||
|
_turnElapsed = 0;
|
||||||
|
_beatPlayed = false;
|
||||||
|
}
|
||||||
|
_turnElapsed += dt;
|
||||||
|
|
||||||
|
final dur = (s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000.0 : 30.0);
|
||||||
|
final remaining = dur - _turnElapsed;
|
||||||
|
final warn = math.min(8.0, dur * 0.4);
|
||||||
|
// فقط یکبار، دقیقاً وقتی به آستانهی پایانی رسیدیم.
|
||||||
|
if (!_beatPlayed && remaining > 0 && remaining <= warn) {
|
||||||
|
_beatPlayed = true;
|
||||||
|
AppSounds.heartBeat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -222,7 +275,7 @@ class HokmGame extends FlameGame {
|
|||||||
void _relayout() {
|
void _relayout() {
|
||||||
final s = _s;
|
final s = _s;
|
||||||
if (s == null) return;
|
if (s == null) return;
|
||||||
_cardW = size.x * 0.19;
|
_cardW = size.x * 0.22; // کارتهای روی میز بزرگتر (دیدِ بهتر)
|
||||||
_cardH = _cardW * kCardRatio;
|
_cardH = _cardW * kCardRatio;
|
||||||
|
|
||||||
_rebuildBacksAndInfo(s);
|
_rebuildBacksAndInfo(s);
|
||||||
@@ -244,12 +297,19 @@ class HokmGame extends FlameGame {
|
|||||||
|
|
||||||
// حرکتِ نرمِ یک کارت به مقصد، بدون انباشتهشدنِ افکتها.
|
// حرکتِ نرمِ یک کارت به مقصد، بدون انباشتهشدنِ افکتها.
|
||||||
void _moveTo(CardComponent c, Vector2 target, {double dur = 0.38}) {
|
void _moveTo(CardComponent c, Vector2 target, {double dur = 0.38}) {
|
||||||
c.children.whereType<MoveToEffect>().toList().forEach((e) => e.removeFromParent());
|
c.children.whereType<MoveToEffect>().toList().forEach(
|
||||||
|
(e) => e.removeFromParent(),
|
||||||
|
);
|
||||||
if ((c.position - target).length < 0.5) {
|
if ((c.position - target).length < 0.5) {
|
||||||
c.position = target;
|
c.position = target;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
c.add(MoveToEffect(target, EffectController(duration: dur, curve: Curves.easeOutCubic)));
|
c.add(
|
||||||
|
MoveToEffect(
|
||||||
|
target,
|
||||||
|
EffectController(duration: dur, curve: Curves.easeOutCubic),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ناحیهی وسط میز که رهاکردنِ کارت در آن یعنی بازی (بالاتر از دستِ شما).
|
// ناحیهی وسط میز که رهاکردنِ کارت در آن یعنی بازی (بالاتر از دستِ شما).
|
||||||
@@ -270,11 +330,12 @@ class HokmGame extends FlameGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// چیدمانِ بادبزنیِ منحنی: کارتها چرخش و قوسِ ملایم دارند (مثل دستِ واقعی).
|
// چیدمانِ بادبزنیِ منحنی: کارتها چرخش و قوسِ ملایم دارند (مثل دستِ واقعی).
|
||||||
final hw = size.x * 0.225, hh = hw * kCardRatio;
|
// اندازهی دست کمی بزرگتر از کارتهای روی میز است (خوانا ولی نه نامتوازن).
|
||||||
final handW = size.x * 0.86;
|
final hw = size.x * 0.24, hh = hw * kCardRatio;
|
||||||
|
final handW = size.x * 0.90;
|
||||||
final stepX = n > 1 ? ((handW - hw) / (n - 1)).clamp(0.0, hw * 0.62) : 0.0;
|
final stepX = n > 1 ? ((handW - hw) / (n - 1)).clamp(0.0, hw * 0.62) : 0.0;
|
||||||
final cx = size.x / 2;
|
final cx = size.x / 2;
|
||||||
final baseY = size.y * 0.86;
|
final baseY = size.y * 0.83;
|
||||||
final tMax = (n - 1) / 2;
|
final tMax = (n - 1) / 2;
|
||||||
const edgeAngle = 0.34; // چرخشِ کارتهای کناری (رادیان)
|
const edgeAngle = 0.34; // چرخشِ کارتهای کناری (رادیان)
|
||||||
final dip = hh * 0.16; // افتِ عمودیِ کارتهای کناری برای قوس
|
final dip = hh * 0.16; // افتِ عمودیِ کارتهای کناری برای قوس
|
||||||
@@ -288,7 +349,12 @@ class HokmGame extends FlameGame {
|
|||||||
var c = _hand[code];
|
var c = _hand[code];
|
||||||
if (c == null) {
|
if (c == null) {
|
||||||
// کارت جدید ⇒ از مرکز میز پخش میشود.
|
// کارت جدید ⇒ از مرکز میز پخش میشود.
|
||||||
c = CardComponent(code: code, faceUp: true, size: Vector2(hw, hh), position: size / 2);
|
c = CardComponent(
|
||||||
|
code: code,
|
||||||
|
faceUp: true,
|
||||||
|
size: Vector2(hw, hh),
|
||||||
|
position: size / 2,
|
||||||
|
);
|
||||||
_hand[code] = c;
|
_hand[code] = c;
|
||||||
add(c);
|
add(c);
|
||||||
} else {
|
} else {
|
||||||
@@ -312,12 +378,18 @@ class HokmGame extends FlameGame {
|
|||||||
for (final code in _trick.keys.toList()) {
|
for (final code in _trick.keys.toList()) {
|
||||||
if (present.contains(code)) continue;
|
if (present.contains(code)) continue;
|
||||||
final c = _trick.remove(code)!;
|
final c = _trick.remove(code)!;
|
||||||
c.children.whereType<MoveToEffect>().toList().forEach((e) => e.removeFromParent());
|
c.children.whereType<MoveToEffect>().toList().forEach(
|
||||||
c.add(SequenceEffect([
|
(e) => e.removeFromParent(),
|
||||||
MoveToEffect(_seatOrigin(_rel(s.turn)),
|
);
|
||||||
EffectController(duration: 0.25, curve: Curves.easeIn)),
|
c.add(
|
||||||
|
SequenceEffect([
|
||||||
|
MoveToEffect(
|
||||||
|
_seatOrigin(_rel(s.turn)),
|
||||||
|
EffectController(duration: 0.25, curve: Curves.easeIn),
|
||||||
|
),
|
||||||
RemoveEffect(),
|
RemoveEffect(),
|
||||||
]));
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final tc in s.trick) {
|
for (final tc in s.trick) {
|
||||||
@@ -330,6 +402,7 @@ class HokmGame extends FlameGame {
|
|||||||
c.onPlay = null;
|
c.onPlay = null;
|
||||||
c.dimmed = false;
|
c.dimmed = false;
|
||||||
c.home = null;
|
c.home = null;
|
||||||
|
c.resetScale(); // کارتِ بازیشده هماندازهی بقیهی کارتهای میز
|
||||||
} else {
|
} else {
|
||||||
c = CardComponent(
|
c = CardComponent(
|
||||||
code: tc.card,
|
code: tc.card,
|
||||||
@@ -409,7 +482,8 @@ class HokmGame extends FlameGame {
|
|||||||
final mine = team == s.yourSeat % 2;
|
final mine = team == s.yourSeat % 2;
|
||||||
final w = _cardW * 0.40, h = w * kCardRatio;
|
final w = _cardW * 0.40, h = w * kCardRatio;
|
||||||
// تیم شما جلوی شما (پایین)، تیم حریف جلوی حریفِ سمت راست.
|
// تیم شما جلوی شما (پایین)، تیم حریف جلوی حریفِ سمت راست.
|
||||||
final base = mine
|
final base =
|
||||||
|
mine
|
||||||
? Vector2(size.x * 0.28, size.y * 0.72)
|
? Vector2(size.x * 0.28, size.y * 0.72)
|
||||||
: Vector2(size.x * 0.82, size.y * 0.24);
|
: Vector2(size.x * 0.82, size.y * 0.24);
|
||||||
final step = Vector2(w * 0.40, 0);
|
final step = Vector2(w * 0.40, 0);
|
||||||
@@ -429,10 +503,12 @@ class HokmGame extends FlameGame {
|
|||||||
for (var team = 0; team < 2; team++) {
|
for (var team = 0; team < 2; team++) {
|
||||||
final score = team < s.scores.length ? s.scores[team] : 0;
|
final score = team < s.scores.length ? s.scores[team] : 0;
|
||||||
final mine = team == s.yourSeat % 2;
|
final mine = team == s.yourSeat % 2;
|
||||||
final pos = mine
|
final pos =
|
||||||
|
mine
|
||||||
? Vector2(size.x * 0.50, size.y * 0.74)
|
? Vector2(size.x * 0.50, size.y * 0.74)
|
||||||
: Vector2(size.x * 0.84, size.y * 0.44);
|
: Vector2(size.x * 0.84, size.y * 0.44);
|
||||||
final puck = ScorePuck(score, radius: radius, position: pos)..priority = 22;
|
final puck = ScorePuck(score, radius: radius, position: pos)
|
||||||
|
..priority = 22;
|
||||||
_info.add(puck);
|
_info.add(puck);
|
||||||
add(puck);
|
add(puck);
|
||||||
}
|
}
|
||||||
@@ -467,13 +543,23 @@ class HokmGame extends FlameGame {
|
|||||||
void _addInfo(GameState s) {
|
void _addInfo(GameState s) {
|
||||||
if (s.trump != null) {
|
if (s.trump != null) {
|
||||||
final (sym, col) = _trumpGlyph(s.trump!);
|
final (sym, col) = _trumpGlyph(s.trump!);
|
||||||
_addLabel('حکم: $sym', Vector2(size.x * 0.04, size.y * 0.04), size.x * 0.05,
|
_addLabel(
|
||||||
color: col, anchor: Anchor.topLeft, bold: true);
|
'حکم: $sym',
|
||||||
|
Vector2(size.x * 0.04, size.y * 0.04),
|
||||||
|
size.x * 0.05,
|
||||||
|
color: col,
|
||||||
|
anchor: Anchor.topLeft,
|
||||||
|
bold: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final a = s.scores.isNotEmpty ? s.scores[0] : 0;
|
final a = s.scores.isNotEmpty ? s.scores[0] : 0;
|
||||||
final b = s.scores.length > 1 ? s.scores[1] : 0;
|
final b = s.scores.length > 1 ? s.scores[1] : 0;
|
||||||
_addLabel('$a - $b', Vector2(size.x / 2, size.y * 0.04), size.x * 0.05,
|
_addLabel(
|
||||||
anchor: Anchor.topCenter);
|
'$a - $b',
|
||||||
|
Vector2(size.x / 2, size.y * 0.04),
|
||||||
|
size.x * 0.05,
|
||||||
|
anchor: Anchor.topCenter,
|
||||||
|
);
|
||||||
|
|
||||||
for (final p in s.players) {
|
for (final p in s.players) {
|
||||||
final pos = _seatLabelPos(_rel(p.seat));
|
final pos = _seatLabelPos(_rel(p.seat));
|
||||||
@@ -486,22 +572,27 @@ class HokmGame extends FlameGame {
|
|||||||
bold: isTurn,
|
bold: isTurn,
|
||||||
);
|
);
|
||||||
// تاجِ حاکم (روی همهی فازها نشان داده میشود تا حاکم مشخص باشد).
|
// تاجِ حاکم (روی همهی فازها نشان داده میشود تا حاکم مشخص باشد).
|
||||||
|
// y را داخلِ صفحه نگه میداریم؛ برای بازیکنِ بالا، تاج زیرِ نام مینشیند.
|
||||||
if (s.hakem == p.seat) {
|
if (s.hakem == p.seat) {
|
||||||
final crown = CrownBadge(size.x * 0.06, pos - Vector2(0, size.y * 0.066))
|
final above = pos.y > size.y * 0.5; // پایین/کنار ⇒ تاج بالای نام
|
||||||
..priority = 23;
|
final cy = above
|
||||||
|
? pos.y - size.y * 0.066
|
||||||
|
: pos.y + size.y * 0.05; // بازیکنِ بالا ⇒ تاج پایینِ نام
|
||||||
|
final crown = CrownBadge(size.x * 0.07, Vector2(pos.x, cy))
|
||||||
|
..priority = 25;
|
||||||
_info.add(crown);
|
_info.add(crown);
|
||||||
add(crown);
|
add(crown);
|
||||||
}
|
}
|
||||||
if (isTurn) {
|
if (isTurn) {
|
||||||
final tpos = pos - Vector2(0, size.y * 0.03);
|
final tpos = pos - Vector2(0, size.y * 0.03);
|
||||||
final human = !p.bot && p.connected;
|
final human = !p.bot && p.connected;
|
||||||
// حلقهی شمارشِ نوبت برای بازیکنِ انسان.
|
// حلقهی شمارشِ نوبت برای بازیکنِ انسان. اگر سرور مهلت را نفرستد،
|
||||||
if (human &&
|
// پیشفرضِ ۳۰ ثانیه استفاده میشود تا شمارنده همیشه نمایش داده شود.
|
||||||
s.turnTimeoutMs > 0 &&
|
if (human && (s.phase == 'playing' || s.phase == 'choose_trump')) {
|
||||||
(s.phase == 'playing' || s.phase == 'choose_trump')) {
|
final dur = s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000.0 : 30.0;
|
||||||
final timer = TurnTimer(
|
final timer = TurnTimer(
|
||||||
radius: size.x * 0.03,
|
radius: size.x * 0.045,
|
||||||
durationSeconds: s.turnTimeoutMs / 1000.0,
|
durationSeconds: dur,
|
||||||
position: tpos,
|
position: tpos,
|
||||||
)..priority = 20;
|
)..priority = 20;
|
||||||
_info.add(timer);
|
_info.add(timer);
|
||||||
@@ -519,15 +610,25 @@ class HokmGame extends FlameGame {
|
|||||||
// ===== کمکیهای ساختِ عنصر (ثبت در فهرستِ بازساختهشونده) =====
|
// ===== کمکیهای ساختِ عنصر (ثبت در فهرستِ بازساختهشونده) =====
|
||||||
|
|
||||||
void _addBack(Vector2 pos, Vector2 sz, {required int priority}) {
|
void _addBack(Vector2 pos, Vector2 sz, {required int priority}) {
|
||||||
final c = CardComponent(code: '', faceUp: false, size: sz, position: pos, priority: priority);
|
final c = CardComponent(
|
||||||
|
code: '',
|
||||||
|
faceUp: false,
|
||||||
|
size: sz,
|
||||||
|
position: pos,
|
||||||
|
priority: priority,
|
||||||
|
);
|
||||||
_backs.add(c);
|
_backs.add(c);
|
||||||
add(c);
|
add(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addLabel(String text, Vector2 pos, double fontSize,
|
void _addLabel(
|
||||||
{Color color = const Color(0xFFE9B949),
|
String text,
|
||||||
|
Vector2 pos,
|
||||||
|
double fontSize, {
|
||||||
|
Color color = const Color(0xFFE9B949),
|
||||||
Anchor anchor = Anchor.center,
|
Anchor anchor = Anchor.center,
|
||||||
bool bold = false}) {
|
bool bold = false,
|
||||||
|
}) {
|
||||||
final t = TextComponent(
|
final t = TextComponent(
|
||||||
text: text,
|
text: text,
|
||||||
anchor: anchor,
|
anchor: anchor,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import 'package:flame/components.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// حلقهی شمارشِ معکوسِ نوبت: کمانی که در طولِ مهلتِ نوبت خالی میشود.
|
/// حلقهی شمارشِ معکوسِ نوبت: کمانی که در طولِ مهلتِ نوبت خالی میشود.
|
||||||
/// رنگ از طلایی به قرمز میرود و در ثانیههای پایانی هشدار میدهد.
|
/// در ثانیههای پایانی قرمز و ضرباندار میشود. (صدای ضربانِ قلب در HokmGame
|
||||||
|
/// مدیریت میشود تا هنگامِ بازیشدنِ کارت بلافاصله قطع شود.)
|
||||||
class TurnTimer extends PositionComponent {
|
class TurnTimer extends PositionComponent {
|
||||||
final double radius;
|
final double radius;
|
||||||
final double durationSeconds;
|
final double durationSeconds;
|
||||||
@@ -20,6 +21,8 @@ class TurnTimer extends PositionComponent {
|
|||||||
anchor: Anchor.center,
|
anchor: Anchor.center,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
double get _warnSecs => math.min(8.0, durationSeconds * 0.4);
|
||||||
|
|
||||||
static final _bg = Paint()
|
static final _bg = Paint()
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 3
|
..strokeWidth = 3
|
||||||
@@ -28,6 +31,13 @@ class TurnTimer extends PositionComponent {
|
|||||||
@override
|
@override
|
||||||
void update(double dt) {
|
void update(double dt) {
|
||||||
_elapsed += dt;
|
_elapsed += dt;
|
||||||
|
final remaining = durationSeconds - _elapsed;
|
||||||
|
// ضربانِ تصویری در پنجرهی هشدار.
|
||||||
|
if (remaining > 0 && remaining <= _warnSecs) {
|
||||||
|
scale = Vector2.all(1 + 0.18 * math.sin(_elapsed * 2 * math.pi / 0.7).abs());
|
||||||
|
} else {
|
||||||
|
scale = Vector2.all(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,11 +45,12 @@ class TurnTimer extends PositionComponent {
|
|||||||
final center = Offset(radius, radius);
|
final center = Offset(radius, radius);
|
||||||
canvas.drawCircle(center, radius, _bg);
|
canvas.drawCircle(center, radius, _bg);
|
||||||
|
|
||||||
final frac =
|
final frac = durationSeconds <= 0
|
||||||
durationSeconds <= 0 ? 0.0 : (1 - _elapsed / durationSeconds).clamp(0.0, 1.0);
|
? 0.0
|
||||||
|
: (1 - _elapsed / durationSeconds).clamp(0.0, 1.0);
|
||||||
if (frac <= 0) return;
|
if (frac <= 0) return;
|
||||||
|
|
||||||
final color = frac > 0.3 ? const Color(0xFFE9B949) : const Color(0xFFE53935);
|
final color = frac > 0.35 ? const Color(0xFFE9B949) : const Color(0xFFE53935);
|
||||||
final arc = Paint()
|
final arc = Paint()
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 4
|
..strokeWidth = 4
|
||||||
@@ -47,7 +58,7 @@ class TurnTimer extends PositionComponent {
|
|||||||
..color = color;
|
..color = color;
|
||||||
canvas.drawArc(
|
canvas.drawArc(
|
||||||
Rect.fromCircle(center: center, radius: radius),
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
-math.pi / 2, // شروع از بالا
|
-math.pi / 2,
|
||||||
2 * math.pi * frac,
|
2 * math.pi * frac,
|
||||||
false,
|
false,
|
||||||
arc,
|
arc,
|
||||||
|
|||||||
Reference in New Issue
Block a user