feat: phase 1 of improve
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.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_state.dart';
|
||||
import '../../../chat/presentation/widgets/chat_panel.dart';
|
||||
import '../widgets/confetti.dart';
|
||||
import '../widgets/flame/hokm_game.dart';
|
||||
import '../widgets/table_hud.dart';
|
||||
|
||||
@@ -94,7 +96,10 @@ class _GameScreenState extends State<GameScreen> {
|
||||
final won =
|
||||
state.gameOver!.winnerTeam ==
|
||||
(state.state?.yourSeat ?? 0) % 2;
|
||||
if (won) AppSounds.win();
|
||||
if (won) {
|
||||
AppSounds.win();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
if (state.notice != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -619,60 +624,114 @@ class _GameScreenState extends State<GameScreen> {
|
||||
final g = s.gameOver!;
|
||||
final mySeat = s.state?.yourSeat ?? 0;
|
||||
final won = g.winnerTeam == mySeat % 2;
|
||||
return Container(
|
||||
color: Colors.black87,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
won ? 'بردید! 🎉' : 'باختید',
|
||||
style: TextStyle(
|
||||
color: won ? AppColors.gold : Colors.redAccent,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
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 (_tournament != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
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: [
|
||||
const Icon(Icons.emoji_events, color: AppColors.gold, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'+${won ? 100 : 25} امتیاز تورنومنت «${_tournament!.title}»',
|
||||
style: const TextStyle(
|
||||
color: AppColors.gold,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold),
|
||||
return Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: ColoredBox(color: Colors.black87)),
|
||||
if (won) const Positioned.fill(child: Confetti()),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// مدالِ متحرک (تاجِ طلایی برای برد، نشانِ ملایم برای باخت).
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 650),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (_, v, __) => Transform.scale(
|
||||
scale: v.clamp(0.0, 1.2),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: won
|
||||
? const [AppColors.gold, AppColors.goldDark]
|
||||
: [Colors.grey.shade700, Colors.grey.shade900],
|
||||
),
|
||||
border: Border.all(
|
||||
color: won ? Colors.white70 : Colors.white24,
|
||||
width: 2),
|
||||
boxShadow: won
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.gold
|
||||
.withValues(alpha: 0.55),
|
||||
blurRadius: 28),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Icon(
|
||||
won
|
||||
? Icons.emoji_events
|
||||
: Icons.sentiment_dissatisfied_outlined,
|
||||
color: won ? AppColors.lapisInk : Colors.white54,
|
||||
size: 54,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 28),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _exitToLobby(context),
|
||||
child: const Text('بازگشت به لابی'),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
won ? 'بردید! 🎉' : 'باختید',
|
||||
style: TextStyle(
|
||||
color: won ? AppColors.gold : Colors.redAccent,
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: const [Shadow(color: Colors.black, blurRadius: 8)],
|
||||
),
|
||||
),
|
||||
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/events.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
|
||||
import '../../../../../core/network/card_images.dart';
|
||||
import '../../../../../core/theme/app_theme.dart';
|
||||
@@ -17,6 +18,7 @@ class CardComponent extends PositionComponent
|
||||
VoidCallback? onPlay; // در صورت مجاز بودن، بازیِ این کارت
|
||||
bool dimmed; // کارت غیرمجاز/غیرفعال
|
||||
Vector2? home; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص)
|
||||
bool highlight = false; // کارتِ برندهی دست ⇒ درخششِ طلایی پیش از جمعشدن
|
||||
int restPriority = 0; // ترتیب لایهی اصلی در دست (برای بازگردانی پس از کشیدن)
|
||||
Rect? dropZone; // ناحیهی وسط میز؛ رهاکردن کارت در آن یعنی بازی
|
||||
Sprite? _front;
|
||||
@@ -76,6 +78,15 @@ class CardComponent extends PositionComponent
|
||||
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
|
||||
void onDragStart(DragStartEvent event) {
|
||||
super.onDragStart(event);
|
||||
@@ -101,6 +112,7 @@ class CardComponent extends PositionComponent
|
||||
// وگرنه برگشت به جای مرتبِ خود.
|
||||
if (_inDropZone()) {
|
||||
resetScale();
|
||||
HapticFeedback.selectionClick(); // بازخوردِ لمسیِ بازیِ کارت
|
||||
onPlay?.call();
|
||||
} else {
|
||||
_scaleTo(1);
|
||||
@@ -184,6 +196,18 @@ class CardComponent extends PositionComponent
|
||||
..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) {
|
||||
canvas.drawRRect(
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
|
||||
import '../../../../../core/service/app_sounds.dart';
|
||||
import '../../../../../core/theme/app_theme.dart';
|
||||
@@ -46,6 +47,7 @@ class HokmGame extends FlameGame {
|
||||
double _shake = 0;
|
||||
double _t = 0;
|
||||
List<String> _prevTrick = const [];
|
||||
bool _highlightedTrick = false; // آیا کارتِ برندهی دستِ کامل هایلایت شده (یکبار)
|
||||
int _bestTrumpRank = 0; // بالاترین رتبهی حکمِ بازیشده در دستِ جاری (برای افکتِ بریدن)
|
||||
int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن
|
||||
String _prevHandSig = ''; // امضای دست؛ برای تشخیصِ بُرِ مجدد (تغییرِ ۵ کارت در فازِ حکم)
|
||||
@@ -171,6 +173,7 @@ class HokmGame extends FlameGame {
|
||||
_shake = 1.0;
|
||||
add(Lightning());
|
||||
AppSounds.cut(); // بریدن با حکم (شمشیر)
|
||||
HapticFeedback.heavyImpact(); // ضربهی لمسیِ بُرِش با حکم
|
||||
} else {
|
||||
AppSounds.placeCard(); // کارت روی میز نشست
|
||||
}
|
||||
@@ -475,22 +478,30 @@ class HokmGame extends FlameGame {
|
||||
}
|
||||
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()) {
|
||||
if (present.contains(code)) continue;
|
||||
final c = _trick.remove(code)!;
|
||||
c.children.whereType<MoveToEffect>().toList().forEach(
|
||||
(e) => e.removeFromParent(),
|
||||
);
|
||||
c.add(
|
||||
SequenceEffect([
|
||||
MoveToEffect(
|
||||
_seatOrigin(_rel(s.turn)),
|
||||
EffectController(duration: 0.25, curve: Curves.easeIn),
|
||||
),
|
||||
RemoveEffect(),
|
||||
]),
|
||||
);
|
||||
c.highlight = false;
|
||||
c.priority = 30; // رویِ همه هنگام جمعشدن
|
||||
c.children.whereType<Effect>().toList().forEach((e) => e.removeFromParent());
|
||||
final delay = i * 0.05; // ترتیبِ کوتاه برای حسِ «جارو کردن»
|
||||
c.add(ScaleEffect.to(
|
||||
Vector2.all(0.5),
|
||||
EffectController(duration: 0.34, startDelay: delay, curve: Curves.easeIn),
|
||||
));
|
||||
c.add(SequenceEffect([
|
||||
MoveToEffect(
|
||||
winnerOrigin,
|
||||
EffectController(
|
||||
duration: 0.36, startDelay: delay, curve: Curves.easeInBack),
|
||||
),
|
||||
RemoveEffect(),
|
||||
]));
|
||||
i++;
|
||||
}
|
||||
|
||||
for (final tc in s.trick) {
|
||||
@@ -521,6 +532,45 @@ class HokmGame extends FlameGame {
|
||||
c.priority = 5;
|
||||
_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(
|
||||
RRect.fromRectAndRadius(rect.deflate(w * 0.018), Radius.circular(h * 0.04)),
|
||||
|
||||
Reference in New Issue
Block a user