feat: phase 1 of improve
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show HapticFeedback;
|
||||||
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 'package:random_avatar/random_avatar.dart';
|
||||||
@@ -20,6 +21,7 @@ import '../../domain/entities/game_entities.dart';
|
|||||||
import '../bloc/game_bloc.dart';
|
import '../bloc/game_bloc.dart';
|
||||||
import '../bloc/game_state.dart';
|
import '../bloc/game_state.dart';
|
||||||
import '../../../chat/presentation/widgets/chat_panel.dart';
|
import '../../../chat/presentation/widgets/chat_panel.dart';
|
||||||
|
import '../widgets/confetti.dart';
|
||||||
import '../widgets/flame/hokm_game.dart';
|
import '../widgets/flame/hokm_game.dart';
|
||||||
import '../widgets/table_hud.dart';
|
import '../widgets/table_hud.dart';
|
||||||
|
|
||||||
@@ -94,7 +96,10 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
final won =
|
final won =
|
||||||
state.gameOver!.winnerTeam ==
|
state.gameOver!.winnerTeam ==
|
||||||
(state.state?.yourSeat ?? 0) % 2;
|
(state.state?.yourSeat ?? 0) % 2;
|
||||||
if (won) AppSounds.win();
|
if (won) {
|
||||||
|
AppSounds.win();
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state.notice != null) {
|
if (state.notice != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -619,60 +624,114 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
final g = s.gameOver!;
|
final g = s.gameOver!;
|
||||||
final mySeat = s.state?.yourSeat ?? 0;
|
final mySeat = s.state?.yourSeat ?? 0;
|
||||||
final won = g.winnerTeam == mySeat % 2;
|
final won = g.winnerTeam == mySeat % 2;
|
||||||
return Container(
|
return Stack(
|
||||||
color: Colors.black87,
|
children: [
|
||||||
alignment: Alignment.center,
|
const Positioned.fill(child: ColoredBox(color: Colors.black87)),
|
||||||
child: Column(
|
if (won) const Positioned.fill(child: Confetti()),
|
||||||
mainAxisSize: MainAxisSize.min,
|
Center(
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
Text(
|
child: Column(
|
||||||
won ? 'بردید! 🎉' : 'باختید',
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: TextStyle(
|
children: [
|
||||||
color: won ? AppColors.gold : Colors.redAccent,
|
// مدالِ متحرک (تاجِ طلایی برای برد، نشانِ ملایم برای باخت).
|
||||||
fontSize: 32,
|
TweenAnimationBuilder<double>(
|
||||||
fontWeight: FontWeight.bold,
|
tween: Tween(begin: 0, end: 1),
|
||||||
),
|
duration: const Duration(milliseconds: 650),
|
||||||
),
|
curve: Curves.easeOutBack,
|
||||||
const SizedBox(height: 12),
|
builder: (_, v, __) => Transform.scale(
|
||||||
Text(
|
scale: v.clamp(0.0, 1.2),
|
||||||
'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}',
|
child: Container(
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 18),
|
width: 100,
|
||||||
),
|
height: 100,
|
||||||
// امتیازِ کسبشدهی تورنومنت (برد ۱۰۰، شرکت ۲۵ — قانونِ ثابتِ سرور).
|
decoration: BoxDecoration(
|
||||||
if (_tournament != null) ...[
|
shape: BoxShape.circle,
|
||||||
const SizedBox(height: 16),
|
gradient: LinearGradient(
|
||||||
Container(
|
colors: won
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
? const [AppColors.gold, AppColors.goldDark]
|
||||||
decoration: BoxDecoration(
|
: [Colors.grey.shade700, Colors.grey.shade900],
|
||||||
color: AppColors.gold.withValues(alpha: 0.12),
|
),
|
||||||
borderRadius: BorderRadius.circular(14),
|
border: Border.all(
|
||||||
border: Border.all(color: AppColors.gold),
|
color: won ? Colors.white70 : Colors.white24,
|
||||||
),
|
width: 2),
|
||||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
boxShadow: won
|
||||||
const Icon(Icons.emoji_events, color: AppColors.gold, size: 20),
|
? [
|
||||||
const SizedBox(width: 8),
|
BoxShadow(
|
||||||
Text(
|
color: AppColors.gold
|
||||||
'+${won ? 100 : 25} امتیاز تورنومنت «${_tournament!.title}»',
|
.withValues(alpha: 0.55),
|
||||||
style: const TextStyle(
|
blurRadius: 28),
|
||||||
color: AppColors.gold,
|
]
|
||||||
fontSize: 14,
|
: null,
|
||||||
fontWeight: FontWeight.bold),
|
),
|
||||||
|
child: Icon(
|
||||||
|
won
|
||||||
|
? Icons.emoji_events
|
||||||
|
: Icons.sentiment_dissatisfied_outlined,
|
||||||
|
color: won ? AppColors.lapisInk : Colors.white54,
|
||||||
|
size: 54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]),
|
const SizedBox(height: 16),
|
||||||
),
|
Text(
|
||||||
],
|
won ? 'بردید! 🎉' : 'باختید',
|
||||||
const SizedBox(height: 28),
|
style: TextStyle(
|
||||||
SizedBox(
|
color: won ? AppColors.gold : Colors.redAccent,
|
||||||
width: 220,
|
fontSize: 34,
|
||||||
child: ElevatedButton(
|
fontWeight: FontWeight.bold,
|
||||||
onPressed: () => _exitToLobby(context),
|
shadows: const [Shadow(color: Colors.black, blurRadius: 8)],
|
||||||
child: const Text('بازگشت به لابی'),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}',
|
||||||
|
style: const TextStyle(color: Colors.white70, fontSize: 18),
|
||||||
|
),
|
||||||
|
// پاداشِ سکهی برد (جایزهی میز).
|
||||||
|
if (won && widget.prize > 0) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_rewardChip(
|
||||||
|
Icons.monetization_on, '+${widget.prize} سکه'),
|
||||||
|
],
|
||||||
|
// امتیازِ کسبشدهی تورنومنت (برد ۱۰۰، شرکت ۲۵).
|
||||||
|
if (_tournament != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_rewardChip(Icons.emoji_events,
|
||||||
|
'+${won ? 100 : 25} امتیاز تورنومنت «${_tournament!.title}»'),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
SizedBox(
|
||||||
|
width: 220,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _exitToLobby(context),
|
||||||
|
child: const Text('بازگشت به لابی'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _rewardChip(IconData icon, String text) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.gold.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: AppColors.gold),
|
||||||
|
),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Icon(icon, color: AppColors.gold, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.gold,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// آواتارِ در حالِ تغییر برای جایگاههای خالیِ پنل جستجو (حسِ «در حال یافتن»).
|
/// آواتارِ در حالِ تغییر برای جایگاههای خالیِ پنل جستجو (حسِ «در حال یافتن»).
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// بارشِ کاغذرنگی (کانفتی) سبک و بدونِ وابستگی — برای صفحهی بردِ بازی.
|
||||||
|
class Confetti extends StatefulWidget {
|
||||||
|
final int count;
|
||||||
|
const Confetti({super.key, this.count = 60});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Confetti> createState() => _ConfettiState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfettiState extends State<Confetti> with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _c;
|
||||||
|
late final List<_Piece> _pieces;
|
||||||
|
|
||||||
|
static const _colors = [
|
||||||
|
Color(0xFFE9B949), // طلایی
|
||||||
|
Color(0xFF1E5FA8), // لاجوردی
|
||||||
|
Color(0xFFB81D2A), // شنگرف
|
||||||
|
Color(0xFF3FA34D), // سبز
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final r = math.Random();
|
||||||
|
_pieces = List.generate(widget.count, (_) {
|
||||||
|
return _Piece(
|
||||||
|
x: r.nextDouble(),
|
||||||
|
delay: r.nextDouble() * 0.5,
|
||||||
|
speed: 0.7 + r.nextDouble() * 0.6,
|
||||||
|
drift: (r.nextDouble() - 0.5) * 0.4,
|
||||||
|
size: 6 + r.nextDouble() * 7,
|
||||||
|
color: _colors[r.nextInt(_colors.length)],
|
||||||
|
rot: r.nextDouble() * math.pi,
|
||||||
|
rotSpeed: (r.nextDouble() - 0.5) * 12,
|
||||||
|
round: r.nextBool(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_c = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 2600),
|
||||||
|
)..forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_c.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _c,
|
||||||
|
builder: (_, __) => CustomPaint(
|
||||||
|
size: Size.infinite,
|
||||||
|
painter: _ConfettiPainter(_pieces, _c.value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Piece {
|
||||||
|
final double x, delay, speed, drift, size, rot, rotSpeed;
|
||||||
|
final Color color;
|
||||||
|
final bool round;
|
||||||
|
_Piece({
|
||||||
|
required this.x,
|
||||||
|
required this.delay,
|
||||||
|
required this.speed,
|
||||||
|
required this.drift,
|
||||||
|
required this.size,
|
||||||
|
required this.color,
|
||||||
|
required this.rot,
|
||||||
|
required this.rotSpeed,
|
||||||
|
required this.round,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfettiPainter extends CustomPainter {
|
||||||
|
final List<_Piece> pieces;
|
||||||
|
final double t; // ۰..۱
|
||||||
|
_ConfettiPainter(this.pieces, this.t);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
for (final p in pieces) {
|
||||||
|
final local = ((t - p.delay) * p.speed).clamp(0.0, 1.0);
|
||||||
|
if (local <= 0) continue;
|
||||||
|
final y = -0.1 + local * 1.25;
|
||||||
|
final x = (p.x + p.drift * local) * size.width;
|
||||||
|
final py = y * size.height;
|
||||||
|
final opacity = (1.0 - local).clamp(0.0, 1.0);
|
||||||
|
final paint = Paint()..color = p.color.withValues(alpha: opacity);
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(x, py);
|
||||||
|
canvas.rotate(p.rot + p.rotSpeed * local);
|
||||||
|
if (p.round) {
|
||||||
|
canvas.drawCircle(Offset.zero, p.size / 2, paint);
|
||||||
|
} else {
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromCenter(center: Offset.zero, width: p.size, height: p.size * 0.6),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _ConfettiPainter old) => old.t != t;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flame/components.dart';
|
|||||||
import 'package:flame/effects.dart';
|
import 'package:flame/effects.dart';
|
||||||
import 'package:flame/events.dart';
|
import 'package:flame/events.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show HapticFeedback;
|
||||||
|
|
||||||
import '../../../../../core/network/card_images.dart';
|
import '../../../../../core/network/card_images.dart';
|
||||||
import '../../../../../core/theme/app_theme.dart';
|
import '../../../../../core/theme/app_theme.dart';
|
||||||
@@ -17,6 +18,7 @@ class CardComponent extends PositionComponent
|
|||||||
VoidCallback? onPlay; // در صورت مجاز بودن، بازیِ این کارت
|
VoidCallback? onPlay; // در صورت مجاز بودن، بازیِ این کارت
|
||||||
bool dimmed; // کارت غیرمجاز/غیرفعال
|
bool dimmed; // کارت غیرمجاز/غیرفعال
|
||||||
Vector2? home; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص)
|
Vector2? home; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص)
|
||||||
|
bool highlight = false; // کارتِ برندهی دست ⇒ درخششِ طلایی پیش از جمعشدن
|
||||||
int restPriority = 0; // ترتیب لایهی اصلی در دست (برای بازگردانی پس از کشیدن)
|
int restPriority = 0; // ترتیب لایهی اصلی در دست (برای بازگردانی پس از کشیدن)
|
||||||
Rect? dropZone; // ناحیهی وسط میز؛ رهاکردن کارت در آن یعنی بازی
|
Rect? dropZone; // ناحیهی وسط میز؛ رهاکردن کارت در آن یعنی بازی
|
||||||
Sprite? _front;
|
Sprite? _front;
|
||||||
@@ -76,6 +78,15 @@ class CardComponent extends PositionComponent
|
|||||||
scale = Vector2.all(1);
|
scale = Vector2.all(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// یک «پاپِ» کوتاه (بزرگنماییِ رفتوبرگشتی) برای کارتِ برندهی دست.
|
||||||
|
void pulse() {
|
||||||
|
children.whereType<ScaleEffect>().toList().forEach((e) => e.removeFromParent());
|
||||||
|
add(ScaleEffect.to(
|
||||||
|
Vector2.all(1.16),
|
||||||
|
EffectController(duration: 0.18, alternate: true, curve: Curves.easeOut),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onDragStart(DragStartEvent event) {
|
void onDragStart(DragStartEvent event) {
|
||||||
super.onDragStart(event);
|
super.onDragStart(event);
|
||||||
@@ -101,6 +112,7 @@ class CardComponent extends PositionComponent
|
|||||||
// وگرنه برگشت به جای مرتبِ خود.
|
// وگرنه برگشت به جای مرتبِ خود.
|
||||||
if (_inDropZone()) {
|
if (_inDropZone()) {
|
||||||
resetScale();
|
resetScale();
|
||||||
|
HapticFeedback.selectionClick(); // بازخوردِ لمسیِ بازیِ کارت
|
||||||
onPlay?.call();
|
onPlay?.call();
|
||||||
} else {
|
} else {
|
||||||
_scaleTo(1);
|
_scaleTo(1);
|
||||||
@@ -184,6 +196,18 @@ class CardComponent extends PositionComponent
|
|||||||
..color = AppColors.suitDark,
|
..color = AppColors.suitDark,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// درخششِ طلاییِ کارتِ برندهی دست (فقط یک کارت همزمان ⇒ blur ارزان).
|
||||||
|
if (highlight) {
|
||||||
|
canvas.drawRRect(
|
||||||
|
rrect,
|
||||||
|
Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = size.x * 0.07
|
||||||
|
..color = AppColors.gold
|
||||||
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// هایلایت طلایی: هنگام کشیدن همیشه (انتخاب)، و پررنگتر روی ناحیهی انداختن.
|
// هایلایت طلایی: هنگام کشیدن همیشه (انتخاب)، و پررنگتر روی ناحیهی انداختن.
|
||||||
if (_dragging) {
|
if (_dragging) {
|
||||||
canvas.drawRRect(
|
canvas.drawRRect(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flame/components.dart';
|
|||||||
import 'package:flame/effects.dart';
|
import 'package:flame/effects.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show HapticFeedback;
|
||||||
|
|
||||||
import '../../../../../core/service/app_sounds.dart';
|
import '../../../../../core/service/app_sounds.dart';
|
||||||
import '../../../../../core/theme/app_theme.dart';
|
import '../../../../../core/theme/app_theme.dart';
|
||||||
@@ -46,6 +47,7 @@ class HokmGame extends FlameGame {
|
|||||||
double _shake = 0;
|
double _shake = 0;
|
||||||
double _t = 0;
|
double _t = 0;
|
||||||
List<String> _prevTrick = const [];
|
List<String> _prevTrick = const [];
|
||||||
|
bool _highlightedTrick = false; // آیا کارتِ برندهی دستِ کامل هایلایت شده (یکبار)
|
||||||
int _bestTrumpRank = 0; // بالاترین رتبهی حکمِ بازیشده در دستِ جاری (برای افکتِ بریدن)
|
int _bestTrumpRank = 0; // بالاترین رتبهی حکمِ بازیشده در دستِ جاری (برای افکتِ بریدن)
|
||||||
int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن
|
int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن
|
||||||
String _prevHandSig = ''; // امضای دست؛ برای تشخیصِ بُرِ مجدد (تغییرِ ۵ کارت در فازِ حکم)
|
String _prevHandSig = ''; // امضای دست؛ برای تشخیصِ بُرِ مجدد (تغییرِ ۵ کارت در فازِ حکم)
|
||||||
@@ -171,6 +173,7 @@ class HokmGame extends FlameGame {
|
|||||||
_shake = 1.0;
|
_shake = 1.0;
|
||||||
add(Lightning());
|
add(Lightning());
|
||||||
AppSounds.cut(); // بریدن با حکم (شمشیر)
|
AppSounds.cut(); // بریدن با حکم (شمشیر)
|
||||||
|
HapticFeedback.heavyImpact(); // ضربهی لمسیِ بُرِش با حکم
|
||||||
} else {
|
} else {
|
||||||
AppSounds.placeCard(); // کارت روی میز نشست
|
AppSounds.placeCard(); // کارت روی میز نشست
|
||||||
}
|
}
|
||||||
@@ -475,22 +478,30 @@ class HokmGame extends FlameGame {
|
|||||||
}
|
}
|
||||||
final present = s.trick.map((t) => t.card).toSet();
|
final present = s.trick.map((t) => t.card).toSet();
|
||||||
|
|
||||||
// کارتهای trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده برو و حذف شو.
|
// کارتهای trick که دیگر نیستند (دست جمع شد) ⇒ برنده کارتها را «جمع میکند»:
|
||||||
|
// به سمتِ جایگاهِ برنده (s.turn) پرواز میکنند، کوچک میشوند و محو (scoop).
|
||||||
|
final winnerOrigin = _seatOrigin(_rel(s.turn));
|
||||||
|
var i = 0;
|
||||||
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(
|
c.highlight = false;
|
||||||
(e) => e.removeFromParent(),
|
c.priority = 30; // رویِ همه هنگام جمعشدن
|
||||||
);
|
c.children.whereType<Effect>().toList().forEach((e) => e.removeFromParent());
|
||||||
c.add(
|
final delay = i * 0.05; // ترتیبِ کوتاه برای حسِ «جارو کردن»
|
||||||
SequenceEffect([
|
c.add(ScaleEffect.to(
|
||||||
MoveToEffect(
|
Vector2.all(0.5),
|
||||||
_seatOrigin(_rel(s.turn)),
|
EffectController(duration: 0.34, startDelay: delay, curve: Curves.easeIn),
|
||||||
EffectController(duration: 0.25, curve: Curves.easeIn),
|
));
|
||||||
),
|
c.add(SequenceEffect([
|
||||||
RemoveEffect(),
|
MoveToEffect(
|
||||||
]),
|
winnerOrigin,
|
||||||
);
|
EffectController(
|
||||||
|
duration: 0.36, startDelay: delay, curve: Curves.easeInBack),
|
||||||
|
),
|
||||||
|
RemoveEffect(),
|
||||||
|
]));
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final tc in s.trick) {
|
for (final tc in s.trick) {
|
||||||
@@ -521,6 +532,45 @@ class HokmGame extends FlameGame {
|
|||||||
c.priority = 5;
|
c.priority = 5;
|
||||||
_moveTo(c, slot);
|
_moveTo(c, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// دستِ کامل (۴ کارت) ⇒ کارتِ برنده را طلایی هایلایت کن و یکبار «پاپ» بزن،
|
||||||
|
// تا کاربر پیش از جمعشدنِ کارتها بفهمد چه کسی برد.
|
||||||
|
if (s.trickDone && s.trick.length == 4) {
|
||||||
|
final ws = _winnerSeat(s.trick, s.trump, s.leadSuit);
|
||||||
|
for (final tc in s.trick) {
|
||||||
|
final comp = _trick[tc.card];
|
||||||
|
if (comp == null) continue;
|
||||||
|
final win = tc.seat == ws;
|
||||||
|
comp.highlight = win;
|
||||||
|
if (win && !_highlightedTrick) comp.pulse();
|
||||||
|
}
|
||||||
|
_highlightedTrick = true;
|
||||||
|
} else if (!s.trickDone) {
|
||||||
|
_highlightedTrick = false;
|
||||||
|
for (final c in _trick.values) {
|
||||||
|
c.highlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// برندهی دستِ جاری را از روی کارتها + حکم + خالِ زمینه حساب میکند (سمتِ کلاینت).
|
||||||
|
int _winnerSeat(List<TrickCard> trick, String? trump, String? lead) {
|
||||||
|
var best = trick.first;
|
||||||
|
for (final tc in trick.skip(1)) {
|
||||||
|
if (_beats(tc.card, best.card, trump, lead)) best = tc;
|
||||||
|
}
|
||||||
|
return best.seat;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _beats(String a, String b, String? trump, String? lead) {
|
||||||
|
final aS = suitName(a), bS = suitName(b);
|
||||||
|
final aT = aS == trump, bT = bS == trump;
|
||||||
|
if (aT && !bT) return true;
|
||||||
|
if (!aT && bT) return false;
|
||||||
|
if (aT && bT) return rankValue(a) > rankValue(b);
|
||||||
|
if (aS != lead) return false; // فقط کارتِ همخالِ زمینه میتواند ببرد
|
||||||
|
if (bS != lead) return true;
|
||||||
|
return rankValue(a) > rankValue(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
// مبدأِ نشستنِ هر جایگاه (برای پرتاب/جمعِ کارتها).
|
// مبدأِ نشستنِ هر جایگاه (برای پرتاب/جمعِ کارتها).
|
||||||
|
|||||||
@@ -99,6 +99,21 @@ class Felt extends PositionComponent with HasGameReference {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// نورِ نرمِ مرکزی (اسپاتلایت) برای برجستهشدنِ کارتهای وسطِ میز.
|
||||||
|
canvas.save();
|
||||||
|
canvas.clipRRect(table);
|
||||||
|
canvas.drawRect(
|
||||||
|
rect,
|
||||||
|
Paint()
|
||||||
|
..shader = RadialGradient(
|
||||||
|
center: Alignment.center,
|
||||||
|
radius: 0.62,
|
||||||
|
colors: const [Color(0x1FFFFFFF), Color(0x00FFFFFF)],
|
||||||
|
stops: const [0.0, 1.0],
|
||||||
|
).createShader(rect),
|
||||||
|
);
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
// خطِ طلاییِ نازکِ داخلی (قابِ دولایه).
|
// خطِ طلاییِ نازکِ داخلی (قابِ دولایه).
|
||||||
canvas.drawRRect(
|
canvas.drawRRect(
|
||||||
RRect.fromRectAndRadius(rect.deflate(w * 0.018), Radius.circular(h * 0.04)),
|
RRect.fromRectAndRadius(rect.deflate(w * 0.018), Radius.circular(h * 0.04)),
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||||||
icon: Icons.group_add,
|
icon: Icons.group_add,
|
||||||
label: 'دورهمی',
|
label: 'دورهمی',
|
||||||
sub: 'بازی با دوستان',
|
sub: 'بازی با دوستان',
|
||||||
accent: AppColors.lapis,
|
accent: AppColors.accent,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await context.push('/private');
|
await context.push('/private');
|
||||||
if (context.mounted) _reload();
|
if (context.mounted) _reload();
|
||||||
@@ -122,16 +122,18 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_DailyCard(
|
_DailyCard(
|
||||||
wallet: wallet,
|
wallet: wallet,
|
||||||
onTap: () => StreakDialog.show(
|
onTap:
|
||||||
context,
|
() => StreakDialog.show(
|
||||||
streak: wallet?.dailyStreak ?? 0,
|
context,
|
||||||
best: wallet?.bestStreak ?? 0,
|
streak: wallet?.dailyStreak ?? 0,
|
||||||
ready: wallet?.dailyReady ?? true,
|
best: wallet?.bestStreak ?? 0,
|
||||||
nextReward: wallet?.nextDaily ?? 200,
|
ready: wallet?.dailyReady ?? true,
|
||||||
onClaim: () => context
|
nextReward: wallet?.nextDaily ?? 200,
|
||||||
.read<WalletBloc>()
|
onClaim:
|
||||||
.add(ClaimDailyEvent()),
|
() => context.read<WalletBloc>().add(
|
||||||
),
|
ClaimDailyEvent(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -142,8 +144,11 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||||||
context.read<AuthBloc>().add(LogoutEvent());
|
context.read<AuthBloc>().add(LogoutEvent());
|
||||||
context.go('/login');
|
context.go('/login');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.logout,
|
icon: const Icon(
|
||||||
color: Colors.white38, size: 18),
|
Icons.logout,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'خروج از حساب',
|
'خروج از حساب',
|
||||||
style: TextStyle(color: Colors.white38, fontSize: 13),
|
style: TextStyle(color: Colors.white38, fontSize: 13),
|
||||||
@@ -175,8 +180,10 @@ class _LobbyHeader extends StatelessWidget {
|
|||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
GlowText('حاکم ایرانی', size: 36),
|
GlowText('حاکم ایرانی', size: 36),
|
||||||
SizedBox(height: 2),
|
SizedBox(height: 2),
|
||||||
Text('بازیِ آنلاینِ حکم',
|
Text(
|
||||||
style: TextStyle(color: Colors.white54, fontSize: 13)),
|
'بازیِ آنلاینِ حکم',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 13),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -202,9 +209,14 @@ class _PrimaryPlay extends StatelessWidget {
|
|||||||
border: Border.all(color: AppColors.gold, width: 2),
|
border: Border.all(color: AppColors.gold, width: 2),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.gold.withValues(alpha: 0.28), blurRadius: 16),
|
color: AppColors.gold.withValues(alpha: 0.28),
|
||||||
|
blurRadius: 16,
|
||||||
|
),
|
||||||
const BoxShadow(
|
const BoxShadow(
|
||||||
color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)),
|
color: Colors.black54,
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 5),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -213,30 +225,42 @@ class _PrimaryPlay extends StatelessWidget {
|
|||||||
width: 54,
|
width: 54,
|
||||||
height: 54,
|
height: 54,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient:
|
gradient: LinearGradient(
|
||||||
LinearGradient(colors: [AppColors.gold, AppColors.goldDark]),
|
colors: [AppColors.gold, AppColors.goldDark],
|
||||||
|
),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child:
|
child: const Icon(
|
||||||
const Icon(Icons.play_arrow_rounded, color: AppColors.lapisInk, size: 36),
|
Icons.play_arrow_rounded,
|
||||||
|
color: AppColors.lapisInk,
|
||||||
|
size: 36,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: const [
|
children: const [
|
||||||
Text('بازی سریع',
|
Text(
|
||||||
style: TextStyle(
|
'بازی سریع',
|
||||||
color: Colors.white,
|
style: TextStyle(
|
||||||
fontSize: 22,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold)),
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(height: 2),
|
SizedBox(height: 2),
|
||||||
Text('ورود به میزِ عمومی',
|
Text(
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 13)),
|
'ورود به میزِ عمومی',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 13),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const Icon(Icons.arrow_back_ios_new,
|
const Icon(
|
||||||
color: AppColors.gold, size: 18),
|
Icons.arrow_forward_ios,
|
||||||
|
color: AppColors.gold,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -295,15 +319,19 @@ class _ActionTile extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(label,
|
Text(
|
||||||
style: const TextStyle(
|
label,
|
||||||
color: Colors.white,
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold)),
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(sub,
|
Text(
|
||||||
style:
|
sub,
|
||||||
const TextStyle(color: Colors.white54, fontSize: 11)),
|
style: const TextStyle(color: Colors.white54, fontSize: 11),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -328,9 +356,10 @@ class _DailyCard extends StatelessWidget {
|
|||||||
final ready = w?.dailyReady ?? true;
|
final ready = w?.dailyReady ?? true;
|
||||||
final next = w?.nextDaily ?? 200;
|
final next = w?.nextDaily ?? 200;
|
||||||
|
|
||||||
final colors = ready
|
final colors =
|
||||||
? const [AppColors.success, AppColors.successDark]
|
ready
|
||||||
: const [AppColors.lapisHi, AppColors.lapisLo];
|
? const [AppColors.success, AppColors.successDark]
|
||||||
|
: const [AppColors.lapisHi, AppColors.lapisLo];
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: ready ? 1 : 0.75,
|
opacity: ready ? 1 : 0.75,
|
||||||
@@ -353,17 +382,23 @@ class _DailyCard extends StatelessWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.local_fire_department,
|
Icon(
|
||||||
color: streak > 0 ? const Color(0xFFFF7A1A) : Colors.white54,
|
Icons.local_fire_department,
|
||||||
size: 40),
|
color:
|
||||||
|
streak > 0 ? const Color(0xFFFF7A1A) : Colors.white54,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
if (streak > 0)
|
if (streak > 0)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
child: Text('$streak',
|
child: Text(
|
||||||
style: const TextStyle(
|
'$streak',
|
||||||
color: Colors.white,
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold)),
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -371,14 +406,22 @@ class _DailyCard extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('هدیهی روزانه',
|
const Text(
|
||||||
style: TextStyle(
|
'هدیهی روزانه',
|
||||||
color: Colors.white,
|
style: TextStyle(
|
||||||
fontSize: 16,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold)),
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
streak > 0 ? '$streak روز پیاپی 🔥' : 'زنجیرهات رو شروع کن!',
|
streak > 0
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
? '$streak روز پیاپی 🔥'
|
||||||
|
: 'زنجیرهات رو شروع کن!',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -387,26 +430,42 @@ class _DailyCard extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
Row(
|
||||||
const Icon(Icons.monetization_on,
|
mainAxisSize: MainAxisSize.min,
|
||||||
color: AppColors.gold, size: 16),
|
children: [
|
||||||
const SizedBox(width: 3),
|
const Icon(
|
||||||
Text('+$next',
|
Icons.monetization_on,
|
||||||
|
color: AppColors.gold,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(
|
||||||
|
'+$next',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.gold,
|
color: AppColors.gold,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold,
|
||||||
]),
|
),
|
||||||
const Text('دریافت',
|
),
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 11)),
|
],
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'دریافت',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 11),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Row(mainAxisSize: MainAxisSize.min, children: [
|
const Row(
|
||||||
Icon(Icons.check_circle, color: AppColors.gold, size: 18),
|
mainAxisSize: MainAxisSize.min,
|
||||||
SizedBox(width: 4),
|
children: [
|
||||||
Text('امروز گرفتی',
|
Icon(Icons.check_circle, color: AppColors.gold, size: 18),
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 12)),
|
SizedBox(width: 4),
|
||||||
]),
|
Text(
|
||||||
|
'امروز گرفتی',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user