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'; /// یک کارت روی میز؛ اگر تصویر `assets/images/cards/.png` موجود باشد از آن /// استفاده می‌کند، وگرنه نسخه‌ی برداری می‌کشد. برای پشت کارت `back.jpg`. /// کارت‌های قابل‌بازی فقط با **کشیدن (drag)** به سمت زمین بازی می‌شوند (نه tap). class CardComponent extends PositionComponent with DragCallbacks, HasGameReference { final String code; // مثل "AS"؛ برای پشت کارت خالی final String skin; // اسکینِ کارت؛ تصاویر از backend بارگذاری می‌شوند bool faceUp; // قابلِ تغییر: برای انیمیشنِ برگرداندنِ کارت هنگام پخش VoidCallback? onPlay; // در صورت مجاز بودن، بازیِ این کارت bool dimmed; // کارت غیرمجاز/غیرفعال Vector2? home; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص) bool highlight = false; // کارتِ برنده‌ی دست ⇒ درخششِ طلایی پیش از جمع‌شدن int restPriority = 0; // ترتیب لایه‌ی اصلی در دست (برای بازگردانی پس از کشیدن) Rect? dropZone; // ناحیه‌ی وسط میز؛ رهاکردن کارت در آن یعنی بازی Sprite? _front; Sprite? _back; bool _dragging = false; bool _overZone = false; // کارت روی ناحیه‌ی انداختن است (برای هایلایت) CardComponent({ required this.code, required this.faceUp, this.skin = 'simple', this.onPlay, this.dimmed = false, super.position, super.size, super.angle, super.priority, super.anchor = Anchor.center, }); bool get _playable => onPlay != null && !dimmed; @override Future onLoad() async { if (code.isNotEmpty) { _front = await CardImages.front(skin, code); } _back = await CardImages.back(skin); } /// برگرداندنِ کارت از پشت به رو با انیمیشن (هنگام پخش‌شدن از روی دسته). void flipToFront({double delay = 0}) { if (faceUp) return; add(ScaleEffect.to( Vector2(0.04, scale.y == 0 ? 1 : scale.y), EffectController(duration: 0.13, startDelay: delay, curve: Curves.easeIn), onComplete: () { faceUp = true; // در لبه‌ی نازک، رو می‌شود add(ScaleEffect.to( Vector2(1, 1), EffectController(duration: 0.13, curve: Curves.easeOut))); }, )); } // بزرگ‌نماییِ کارتِ انتخاب‌شده هنگام کشیدن (واضح‌تر برای دیدن). static const _dragScale = 1.4; void _scaleTo(double s) { children.whereType().toList().forEach((e) => e.removeFromParent()); add(ScaleEffect.to( Vector2.all(s), EffectController(duration: 0.12, curve: Curves.easeOut))); } /// بازنشانیِ فوریِ مقیاس به اندازه‌ی عادی (هنگام بازی روی میز). void resetScale() { children.whereType().toList().forEach((e) => e.removeFromParent()); scale = Vector2.all(1); } /// یک «پاپِ» کوتاه (بزرگ‌نماییِ رفت‌وبرگشتی) برای کارتِ برنده‌ی دست. void pulse() { children.whereType().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); if (!_playable) return; _dragging = true; priority = 1000; // روی همه‌ی کارت‌ها _scaleTo(_dragScale); // بزرگ‌نماییِ نرم هنگام انتخاب } @override void onDragUpdate(DragUpdateEvent event) { if (!_dragging) return; position += event.localDelta; _overZone = _inDropZone(); } @override void onDragEnd(DragEndEvent event) { super.onDragEnd(event); if (!_dragging) return; _dragging = false; // فقط اگر داخل ناحیه‌ی وسط میز رها شد ⇒ بازی (اندازه به حالت عادی)؛ // وگرنه برگشت به جای مرتبِ خود. if (_inDropZone()) { resetScale(); HapticFeedback.selectionClick(); // بازخوردِ لمسیِ بازیِ کارت onPlay?.call(); } else { _scaleTo(1); _returnHome(); } _overZone = false; } @override void onDragCancel(DragCancelEvent event) { super.onDragCancel(event); if (!_dragging) return; _dragging = false; _overZone = false; _scaleTo(1); _returnHome(); } bool _inDropZone() { final z = dropZone; if (z != null) return z.contains(position.toOffset()); return position.y < game.size.y * 0.72; // fallback } void _returnHome() { priority = restPriority; // بازگردانی ترتیب لایه تا روی کارت‌های دیگر نیفتد if (home == null) return; add(MoveToEffect(home!, EffectController(duration: 0.2, curve: Curves.easeOut))); } @override void render(Canvas canvas) { final rect = size.toRect(); final radius = Radius.circular(size.x * 0.09); final rrect = RRect.fromRectAndRadius(rect, radius); // سایه‌ی سبک و ارزان (بدون blurِ هر-فریمی) فقط برای کارت‌های رو ⇒ عمق بدون افت کارایی. // (drawShadow هر فریم برای ده‌ها کارت بسیار سنگین بود و باعث لگ می‌شد.) if (faceUp) { final off = _dragging ? size.x * 0.10 : size.x * 0.03; canvas.drawRRect(rrect.shift(Offset(off * 0.4, off)), Paint()..color = Color(_dragging ? 0x66000000 : 0x44000000)); } final sprite = faceUp ? _front : _back; if (sprite != null) { canvas.save(); canvas.clipRRect(rrect); sprite.render(canvas, size: size); canvas.restore(); } else if (faceUp) { _drawVectorFace(canvas, rrect); } else { _drawVectorBack(canvas, rrect); } // براقیتِ ملایم از بالا فقط برای کارت‌های رو. if (faceUp) { canvas.save(); canvas.clipRRect(rrect); canvas.drawRect( Rect.fromLTWH(0, 0, size.x, size.y * 0.5), Paint() ..shader = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x2BFFFFFF), Color(0x00FFFFFF)], ).createShader(Rect.fromLTWH(0, 0, size.x, size.y * 0.5)), ); canvas.restore(); } if (dimmed) { canvas.drawRRect(rrect, Paint()..color = const Color(0x73000000)); } canvas.drawRRect( rrect, Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.5 ..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( rrect, Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.x * (_overZone ? 0.06 : 0.035) ..color = AppColors.gold ..maskFilter = MaskFilter.blur(BlurStyle.normal, _overZone ? 6 : 3), ); } } void _drawVectorBack(Canvas canvas, RRect rrect) { canvas.drawRRect(rrect, Paint()..color = const Color(0xFF1C3A66)); final inner = rrect.deflate(size.x * 0.08); canvas.drawRRect( inner, Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2 ..color = AppColors.gold, ); } void _drawVectorFace(Canvas canvas, RRect rrect) { canvas.drawRRect(rrect, Paint()..color = Colors.white); final rank = code.substring(0, code.length - 1); final suit = code.substring(code.length - 1); final (symbol, color) = _suit(suit); _text(canvas, '$rank$symbol', size.x * 0.26, color, Offset(size.x * 0.08, size.y * 0.05)); // نماد بزرگ وسط _text(canvas, symbol, size.x * 0.5, color, Offset(size.x * 0.5, size.y * 0.5), center: true); } (String, Color) _suit(String s) { switch (s) { case 'H': return ('♥', AppColors.suitRed); case 'D': return ('♦', AppColors.suitRed); case 'C': return ('♣', AppColors.suitDark); default: return ('♠', AppColors.suitDark); } } void _text(Canvas canvas, String s, double fontSize, Color color, Offset at, {bool center = false}) { final tp = TextPainter( text: TextSpan( text: s, style: TextStyle(color: color, fontSize: fontSize)), textDirection: TextDirection.ltr, )..layout(); final offset = center ? at - Offset(tp.width / 2, tp.height / 2) : at; tp.paint(canvas, offset); } }