feat: phase 1 of improve

This commit is contained in:
2026-07-08 00:17:46 +03:30
parent 0bd2b4a18a
commit ffd2646eea
6 changed files with 461 additions and 135 deletions
@@ -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)),