diff --git a/lib/features/game/flame/card_component.dart b/lib/features/game/flame/card_component.dart index 6935052..6f4764e 100644 --- a/lib/features/game/flame/card_component.dart +++ b/lib/features/game/flame/card_component.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame/events.dart'; import 'package:flutter/material.dart'; @@ -7,17 +8,23 @@ const double kCardRatio = 726 / 500; /// یک کارت روی میز؛ اگر تصویر `assets/images/cards/.png` موجود باشد از آن /// استفاده می‌کند، وگرنه نسخه‌ی برداری می‌کشد. برای پشت کارت `back.jpg`. -class CardComponent extends PositionComponent with TapCallbacks, HasGameReference { +/// کارت‌های قابل‌بازی هم با tap و هم با کشیدن (drag) به سمت زمین بازی می‌شوند. +class CardComponent extends PositionComponent + with TapCallbacks, DragCallbacks, HasGameReference { final String code; // مثل "AS"؛ برای پشت کارت خالی final bool faceUp; - VoidCallback? onTap; // با تغییر نوبت/مجاز‌بودن به‌روز می‌شود + VoidCallback? onPlay; // در صورت مجاز بودن، بازیِ این کارت bool dimmed; // کارت غیرمجاز/غیرفعال + Vector2? home; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص) + Rect? dropZone; // ناحیه‌ی وسط میز؛ رهاکردن کارت در آن یعنی بازی Sprite? _sprite; + bool _dragging = false; + bool _overZone = false; // کارت روی ناحیه‌ی انداختن است (برای هایلایت) CardComponent({ required this.code, required this.faceUp, - this.onTap, + this.onPlay, this.dimmed = false, super.position, super.size, @@ -26,6 +33,8 @@ class CardComponent extends PositionComponent with TapCallbacks, HasGameReferenc super.anchor = Anchor.center, }); + bool get _playable => onPlay != null && !dimmed; + @override Future onLoad() async { try { @@ -36,8 +45,60 @@ class CardComponent extends PositionComponent with TapCallbacks, HasGameReferenc } @override - void onTapDown(TapDownEvent event) { - if (onTap != null && !dimmed) onTap!.call(); + void onTapUp(TapUpEvent event) { + if (_playable) onPlay!.call(); + } + + @override + void onDragStart(DragStartEvent event) { + super.onDragStart(event); + if (!_playable) return; + _dragging = true; + priority = 1000; // روی همه‌ی کارت‌ها + scale = Vector2.all(1.12); // بزرگ‌نمایی هنگام کشیدن (فیدبک) + } + + @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; + scale = Vector2.all(1); + // فقط اگر داخل ناحیه‌ی وسط میز رها شد ⇒ بازی؛ وگرنه برگشت به جای مرتبِ خود. + if (_inDropZone()) { + onPlay?.call(); + } else { + _returnHome(); + } + _overZone = false; + } + + @override + void onDragCancel(DragCancelEvent event) { + super.onDragCancel(event); + if (!_dragging) return; + _dragging = false; + _overZone = false; + scale = Vector2.all(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() { + if (home == null) return; + add(MoveToEffect(home!, EffectController(duration: 0.2, curve: Curves.easeOut))); } @override @@ -67,6 +128,18 @@ class CardComponent extends PositionComponent with TapCallbacks, HasGameReferenc ..strokeWidth = 1.5 ..color = const Color(0xFF1A1A1A), ); + + // هایلایت طلایی وقتی کارت روی ناحیه‌ی انداختن است (رهاکنی، بازی می‌شود). + if (_dragging && _overZone) { + canvas.drawRRect( + rrect, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = size.x * 0.06 + ..color = const Color(0xFFE9B949) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6), + ); + } } void _drawVectorBack(Canvas canvas, RRect rrect) { diff --git a/lib/features/game/flame/hokm_game.dart b/lib/features/game/flame/hokm_game.dart index 3185b82..0f3b58e 100644 --- a/lib/features/game/flame/hokm_game.dart +++ b/lib/features/game/flame/hokm_game.dart @@ -25,9 +25,9 @@ class HokmGame extends FlameGame { double _cardW = 60; double _cardH = 87; - double _shake = 0; // شدت تکانِ صفحه (افکت برد با حکم) + double _shake = 0; // شدت تکانِ صفحه (افکت بریدن با حکم) double _t = 0; // زمان برای نوسان تکان - bool _prevTrickDone = false; + List _prevTrick = const []; // کدِ کارت‌های دست جاری (برای تشخیص کارت تازه‌بازی‌شده) HokmGame(this.cubit); @@ -40,18 +40,44 @@ class HokmGame extends FlameGame { _sub = cubit.stream.listen((ui) { if (ui.state != null) { final s = ui.state!; - if (s.trickDone && !_prevTrickDone) _onTrickComplete(s); - _prevTrickDone = s.trickDone; + _checkCut(s); _s = s; _relayout(); } }); if (cubit.state.state != null) { + _prevTrick = cubit.state.state!.trick.map((t) => t.card).toList(); _s = cubit.state.state; _relayout(); } } + // اگر دقیقاً یک کارت تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد + // (خالِ زمینه آتو نیست ولی کارتِ تازه آتوست)، همان لحظه تکان + رعد. + void _checkCut(GameState s) { + final now = s.trick.map((t) => t.card).toList(); + final added = now.length == _prevTrick.length + 1 && + _isPrefix(_prevTrick, now); + if (added) { + final last = now.last; + if (s.trump != null && + s.leadSuit != null && + s.leadSuit != s.trump && + _suitName(last) == s.trump) { + _shake = 1.0; + add(_Lightning()); + } + } + _prevTrick = now; + } + + static bool _isPrefix(List a, List b) { + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + @override void update(double dt) { super.update(dt); @@ -73,37 +99,6 @@ class HokmGame extends FlameGame { } } - // هنگام کامل‌شدن یک دست: اگر برنده «بریده» باشد (خالِ زمینه آتو نبوده ولی - // کارتِ برنده آتوست — یعنی بازیکن چون خال زمینه را نداشت آتو زد) تکان + رعد. - void _onTrickComplete(GameState s) { - if (s.trick.length < 4) return; - final winner = _winnerCard(s); - final cutWithTrump = s.trump != null && - s.leadSuit != s.trump && - _suitName(winner.card) == s.trump; - _shake = cutWithTrump ? 1.0 : 0.3; - if (cutWithTrump) add(_Lightning()); - } - - TrickCard _winnerCard(GameState s) { - var best = s.trick.first; - for (final tc in s.trick.skip(1)) { - if (_beats(tc.card, best.card, s.trump, s.leadSuit)) best = tc; - } - return best; - } - - // آیا کارت a کارت b را می‌برد (با توجه به حکم و خال زمینه)؟ مطابق منطق سرور. - bool _beats(String a, String b, String? trump, String? lead) { - final aT = _suitName(a) == trump, bT = _suitName(b) == trump; - if (aT && !bT) return true; - if (!aT && bT) return false; - if (aT && bT) return _rankValue(a) > _rankValue(b); - if (_suitName(a) != lead) return false; - if (_suitName(b) != lead) return true; - return _rankValue(a) > _rankValue(b); - } - @override void onGameResize(Vector2 size) { super.onGameResize(size); @@ -187,6 +182,14 @@ class HokmGame extends FlameGame { return _rankValue(a) - _rankValue(b); } + // ناحیه‌ی وسط میز که رهاکردن کارت در آن یعنی بازی (بالاتر از دستِ شما). + Rect _dropZone() => Rect.fromLTRB( + size.x * 0.15, + size.y * 0.22, + size.x * 0.85, + size.y * 0.66, + ); + void _layoutHand(GameState s) { // کارت‌های دست بر اساس خال و سپس رتبه مرتب می‌شوند تا کار بازیکن راحت‌تر باشد. final codes = List.from(s.yourHand)..sort(_cardCompare); @@ -200,10 +203,13 @@ class HokmGame extends FlameGame { } } - final handW = size.x * 0.72; - final step = n > 1 ? (handW - _cardW) / (n - 1) : 0.0; + // کارت‌های دستِ خودتان بزرگ‌تر از کارت‌های روی میز. + final hw = size.x * 0.225; + final hh = hw * kCardRatio; + final handW = size.x * 0.80; + final step = n > 1 ? (handW - hw) / (n - 1) : 0.0; final startX = size.x / 2 - (step * (n - 1)) / 2; - final y = size.y * 0.84; + final y = size.y * 0.85; for (var i = 0; i < n; i++) { final code = codes[i]; @@ -216,16 +222,18 @@ class HokmGame extends FlameGame { c = CardComponent( code: code, faceUp: true, - size: Vector2(_cardW, _cardH), + size: Vector2(hw, hh), position: size / 2, ); _hand[code] = c; add(c); } else { - c.size = Vector2(_cardW, _cardH); + c.size = Vector2(hw, hh); } - c.onTap = legal ? () => cubit.playCard(code) : null; + c.onPlay = legal ? () => cubit.playCard(code) : null; c.dimmed = dim; + c.home = target; + c.dropZone = _dropZone(); c.priority = 10 + i; _moveTo(c, target); } @@ -255,8 +263,9 @@ class HokmGame extends FlameGame { // اگر خودِ شما بازی کردید، همان کارتِ دست را منتقل کن (پرواز به وسط). c = _hand.remove(tc.card); if (c != null) { - c.onTap = null; + c.onPlay = null; c.dimmed = false; + c.home = null; } else { c = CardComponent( code: tc.card, @@ -333,9 +342,10 @@ class HokmGame extends FlameGame { if (won <= 0) continue; final mine = team == s.yourSeat % 2; final w = _cardW * 0.40, h = w * kCardRatio; + // تیم شما جلوی شما (پایین)، تیم حریف جلوی حریفِ سمت راست. final base = mine - ? Vector2(size.x * 0.30, size.y * 0.60) - : Vector2(size.x * 0.70, size.y * 0.40); + ? Vector2(size.x * 0.28, size.y * 0.72) + : Vector2(size.x * 0.82, size.y * 0.24); final step = Vector2(w * 0.40, 0); final n = won.clamp(1, 7); final start = base - step * ((n - 1) / 2); @@ -377,9 +387,10 @@ class HokmGame extends FlameGame { for (var team = 0; team < 2; team++) { final score = team < s.scores.length ? s.scores[team] : 0; final mine = team == s.yourSeat % 2; + // یکی جلوی شما (پایین)، دیگری جلوی حریفِ سمت راست — نه جلوی یارِ بالا. final pos = mine - ? Vector2(size.x / 2, size.y * 0.72) // تیم شما: پایینِ مرکز - : Vector2(size.x / 2, size.y * 0.28); // تیم حریف: بالای مرکز + ? Vector2(size.x * 0.50, size.y * 0.74) + : Vector2(size.x * 0.84, size.y * 0.44); final puck = _Puck(score, radius: radius, position: pos)..priority = 22; _info.add(puck); add(puck);