302 lines
11 KiB
Dart
302 lines
11 KiB
Dart
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/<code>.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; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص)
|
|
double homeAngle = 0; // زاویهی بادبزنیِ کارت در دست (برای برگشت پس از کشیدن)
|
|
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<void> onLoad() async {
|
|
// اگر تصویر از قبل در حافظه آماده باشد (از کشِ دیسک/preload)، همان لحظه ست میشود
|
|
// ⇒ کارت با تصویرِ درست و بدونِ پرش رندر میشود. وگرنه بدونِ مسدودکردنِ رندر در
|
|
// پسزمینه بارگذاری و بهمحضِ آمادهشدن جایگزین میشود (نسخهی برداری تا آن لحظه).
|
|
if (code.isNotEmpty) {
|
|
final cached = CardImages.cachedFront(skin, code);
|
|
if (cached != null) {
|
|
_front = cached;
|
|
} else {
|
|
CardImages.front(skin, code).then((s) {
|
|
if (isMounted) _front = s;
|
|
});
|
|
}
|
|
}
|
|
final cb = CardImages.cachedBack(skin);
|
|
if (cb != null) {
|
|
_back = cb;
|
|
} else {
|
|
CardImages.back(skin).then((s) {
|
|
if (isMounted) _back = s;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// برگرداندنِ کارت از پشت به رو با انیمیشن (هنگام پخششدن از روی دسته).
|
|
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<ScaleEffect>().toList().forEach((e) => e.removeFromParent());
|
|
add(ScaleEffect.to(
|
|
Vector2.all(s), EffectController(duration: 0.12, curve: Curves.easeOut)));
|
|
}
|
|
|
|
// چرخاندنِ نرمِ کارت به زاویهی مشخص (صافکردن هنگام کشیدن، برگشت هنگام رها).
|
|
void _rotateTo(double a) {
|
|
children.whereType<RotateEffect>().toList().forEach((e) => e.removeFromParent());
|
|
add(RotateEffect.to(
|
|
a, EffectController(duration: 0.12, curve: Curves.easeOut)));
|
|
}
|
|
|
|
/// بازنشانیِ فوریِ مقیاس به اندازهی عادی (هنگام بازی روی میز).
|
|
void resetScale() {
|
|
children.whereType<ScaleEffect>().toList().forEach((e) => e.removeFromParent());
|
|
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);
|
|
if (!_playable) return;
|
|
_dragging = true;
|
|
priority = 1000; // روی همهی کارتها
|
|
_scaleTo(_dragScale); // بزرگنماییِ نرم هنگام انتخاب
|
|
_rotateTo(0); // صافشدن: کارت دیگر با زاویهی بادبزنیِ دست کشیده نمیشود
|
|
}
|
|
|
|
@override
|
|
void onDragUpdate(DragUpdateEvent event) {
|
|
if (!_dragging) return;
|
|
// canvasDelta (فضای صفحه) نه localDelta: مستقل از چرخش/مقیاسِ خودِ کارت است،
|
|
// پس کارت دقیقاً بهدنبالِ انگشت میآید نه روی مسیرِ منحنیِ قوسِ دست.
|
|
position += event.canvasDelta;
|
|
_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; // بازگردانی ترتیب لایه تا روی کارتهای دیگر نیفتد
|
|
_rotateTo(homeAngle); // بازگشت به زاویهی بادبزنیِ دست
|
|
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);
|
|
}
|
|
}
|