From fbf068c2e6f053791f81a1898a03f37f6c27f8bd Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Mon, 15 Jun 2026 18:57:00 +0330 Subject: [PATCH] feat: clean project --- lib/features/game/flame/card_codes.dart | 46 ++ lib/features/game/flame/card_component.dart | 3 - lib/features/game/flame/hokm_game.dart | 451 ++++++-------------- lib/features/game/flame/table_pieces.dart | 124 ++++++ 4 files changed, 309 insertions(+), 315 deletions(-) create mode 100644 lib/features/game/flame/card_codes.dart create mode 100644 lib/features/game/flame/table_pieces.dart diff --git a/lib/features/game/flame/card_codes.dart b/lib/features/game/flame/card_codes.dart new file mode 100644 index 0000000..ec7e328 --- /dev/null +++ b/lib/features/game/flame/card_codes.dart @@ -0,0 +1,46 @@ +// توابع کمکیِ خالصِ کدِ کوتاهِ کارت‌ها (مثل "AS", "10H", "KD"). +// بدون وابستگی به Flame/Flutter تا قابل‌تست و بازاستفاده باشد. + +/// نسبت ابعاد تصویرِ کارت (۵۰۰×۷۲۶ منبع). +const double kCardRatio = 726 / 500; + +/// نام کاملِ خال از روی آخرین حرفِ کد: S/H/D/C. +String suitName(String code) { + switch (code[code.length - 1]) { + case 'H': + return 'hearts'; + case 'D': + return 'diamonds'; + case 'C': + return 'clubs'; + default: + return 'spades'; + } +} + +/// رتبه‌ی عددی کارت (۲..۱۰ معمولی، J=11، Q=12، K=13، A=14). +int rankValue(String code) { + switch (code.substring(0, code.length - 1)) { + case 'A': + return 14; + case 'K': + return 13; + case 'Q': + return 12; + case 'J': + return 11; + default: + return int.tryParse(code.substring(0, code.length - 1)) ?? 0; + } +} + +// ترتیب خال‌ها برای چیدنِ دست (سیاه/قرمز متناوب تا تفکیک بصری راحت‌تر باشد). +const _suitOrder = {'S': 0, 'H': 1, 'C': 2, 'D': 3}; + +/// مقایسه برای مرتب‌سازی کارت‌های دست: ابتدا خال، سپس رتبه. +int compareCards(String a, String b) { + final sa = _suitOrder[a[a.length - 1]] ?? 0; + final sb = _suitOrder[b[b.length - 1]] ?? 0; + if (sa != sb) return sa - sb; + return rankValue(a) - rankValue(b); +} diff --git a/lib/features/game/flame/card_component.dart b/lib/features/game/flame/card_component.dart index 9d1c45a..d7c1449 100644 --- a/lib/features/game/flame/card_component.dart +++ b/lib/features/game/flame/card_component.dart @@ -3,9 +3,6 @@ import 'package:flame/effects.dart'; import 'package:flame/events.dart'; import 'package:flutter/material.dart'; -/// نسبت ابعاد کارت (۵۰۰×۷۲۶ منبع). -const double kCardRatio = 726 / 500; - /// یک کارت روی میز؛ اگر تصویر `assets/images/cards/.png` موجود باشد از آن /// استفاده می‌کند، وگرنه نسخه‌ی برداری می‌کشد. برای پشت کارت `back.jpg`. /// کارت‌های قابل‌بازی هم با tap و هم با کشیدن (drag) به سمت زمین بازی می‌شوند. diff --git a/lib/features/game/flame/hokm_game.dart b/lib/features/game/flame/hokm_game.dart index 293dc06..f3b63ae 100644 --- a/lib/features/game/flame/hokm_game.dart +++ b/lib/features/game/flame/hokm_game.dart @@ -8,26 +8,38 @@ import 'package:flutter/material.dart'; import '../game_cubit.dart'; import '../game_models.dart'; +import 'card_codes.dart'; import 'card_component.dart'; +import 'table_pieces.dart'; /// صحنه‌ی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید. /// کامپوننت‌های دست و trick ماندگارند و با افکت حرکت جابه‌جا می‌شوند. +/// +/// این کلاس فقط «وضعیتِ سرور → چیدمانِ صحنه» را مدیریت می‌کند: +/// - [_layoutHand] / [_layoutTrick] کارت‌های دست و زمین (ماندگار، با انیمیشن). +/// - [_rebuildBacksAndInfo] عناصرِ بازساخته‌شونده (پشت‌کارت، شمارنده‌ها، برچسب‌ها). +/// - [_checkCut] + [update]/[render] افکتِ «بریدن با حکم» (تکان + رعد). class HokmGame extends FlameGame { final GameCubit cubit; + + // وضعیت بازی و اشتراکِ stream. GameState? _s; StreamSubscription? _sub; - final Map _hand = {}; // کارت‌های دستِ شما (با کد) - final Map _trick = {}; // کارت‌های روی زمین (با کد) - final List _backs = []; // پشت‌کارت حریفان - final List _info = []; // نام/امتیاز/حکم/نوبت + // کامپوننت‌های ماندگار (با کدِ کارت کلید می‌خورند) و بازساخته‌شونده. + final Map _hand = {}; + final Map _trick = {}; + final List _backs = []; + final List _info = []; + // ابعادِ کارت‌های روی میز (بر اساس عرض صفحه محاسبه می‌شود). double _cardW = 60; double _cardH = 87; - double _shake = 0; // شدت تکانِ صفحه (افکت بریدن با حکم) - double _t = 0; // زمان برای نوسان تکان - List _prevTrick = const []; // کدِ کارت‌های دست جاری (برای تشخیص کارت تازه‌بازی‌شده) + // افکتِ تکانِ صفحه هنگام بریدن با حکم. + double _shake = 0; + double _t = 0; + List _prevTrick = const []; HokmGame(this.cubit); @@ -36,14 +48,12 @@ class HokmGame extends FlameGame { @override Future onLoad() async { - add(_Felt()); + add(Felt()); _sub = cubit.stream.listen((ui) { - if (ui.state != null) { - final s = ui.state!; - _checkCut(s); - _s = s; - _relayout(); - } + if (ui.state == null) return; + _checkCut(ui.state!); + _s = ui.state; + _relayout(); }); if (cubit.state.state != null) { _prevTrick = cubit.state.state!.trick.map((t) => t.card).toList(); @@ -52,21 +62,32 @@ class HokmGame extends FlameGame { } } - // اگر دقیقاً یک کارت تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + if (isLoaded) _relayout(); + } + + @override + void onRemove() { + _sub?.cancel(); + super.onRemove(); + } + + // ===== افکتِ بریدن با حکم (تکان + رعد) ===== + + // اگر دقیقاً یک کارتِ تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد // (خالِ زمینه آتو نیست ولی کارتِ تازه آتوست)، همان لحظه تکان + رعد. 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()); - } + final addedOne = now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now); + if (addedOne && + s.trump != null && + s.leadSuit != null && + s.leadSuit != s.trump && + suitName(now.last) == s.trump) { + _shake = 1.0; + add(Lightning()); } _prevTrick = now; } @@ -87,58 +108,27 @@ class HokmGame extends FlameGame { @override void render(Canvas canvas) { - if (_shake > 0) { - final dx = math.sin(_t * 55) * _shake * size.x * 0.018; - final dy = math.cos(_t * 70) * _shake * size.y * 0.010; - canvas.save(); - canvas.translate(dx, dy); - super.render(canvas); - canvas.restore(); - } else { + if (_shake <= 0) { super.render(canvas); + return; } + final dx = math.sin(_t * 55) * _shake * size.x * 0.018; + final dy = math.cos(_t * 70) * _shake * size.y * 0.010; + canvas.save(); + canvas.translate(dx, dy); + super.render(canvas); + canvas.restore(); } - @override - void onGameResize(Vector2 size) { - super.onGameResize(size); - if (isLoaded) _relayout(); - } - - @override - void onRemove() { - _sub?.cancel(); - super.onRemove(); - } + // ===== چیدمان ===== + // جایگاهِ نسبیِ یک seat نسبت به شما: 0=پایین، 1=چپ، 2=بالا، 3=راست. int _rel(int seat) => (seat - _s!.yourSeat + 4) % 4; - static String _suitName(String code) { - switch (code[code.length - 1]) { - case 'H': - return 'hearts'; - case 'D': - return 'diamonds'; - case 'C': - return 'clubs'; - default: - return 'spades'; - } - } - - bool _legal(String code) { - final s = _s!; - if (s.phase != 'playing' || s.trickDone || !s.isMyTurn) return false; - final lead = s.leadSuit; - if (lead == null || lead.isEmpty) return true; - final hasLead = s.yourHand.any((c) => _suitName(c) == lead); - return !hasLead || _suitName(code) == lead; - } - void _relayout() { final s = _s; if (s == null) return; - _cardW = size.x * 0.19; // کارت‌های بزرگ‌تر (مطابق تصویر مرجع) + _cardW = size.x * 0.19; _cardH = _cardW * kCardRatio; _rebuildBacksAndInfo(s); @@ -146,53 +136,33 @@ class HokmGame extends FlameGame { _layoutHand(s); } - // افکت حرکت بدون انباشته‌شدن. + // آیا این کارت در نوبتِ فعلی قابل بازی است (با رعایت follow-suit)؟ + bool _legal(String code) { + final s = _s!; + if (s.phase != 'playing' || s.trickDone || !s.isMyTurn) return false; + final lead = s.leadSuit; + if (lead == null || lead.isEmpty) return true; + final hasLead = s.yourHand.any((c) => suitName(c) == lead); + return !hasLead || suitName(code) == lead; + } + + // حرکتِ نرمِ یک کارت به مقصد، بدون انباشته‌شدنِ افکت‌ها. void _moveTo(CardComponent c, Vector2 target, {double dur = 0.38}) { c.children.whereType().toList().forEach((e) => e.removeFromParent()); if ((c.position - target).length < 0.5) { c.position = target; return; } - c.add(MoveToEffect( - target, EffectController(duration: dur, curve: Curves.easeOutCubic))); + c.add(MoveToEffect(target, EffectController(duration: dur, curve: Curves.easeOutCubic))); } - // ترتیب خال‌ها برای چیدنِ دست (سیاه/قرمز متناوب تا تفکیک بصری راحت‌تر باشد). - static const _suitOrder = {'S': 0, 'H': 1, 'C': 2, 'D': 3}; - - static int _rankValue(String code) { - switch (code.substring(0, code.length - 1)) { - case 'A': - return 14; - case 'K': - return 13; - case 'Q': - return 12; - case 'J': - return 11; - default: - return int.tryParse(code.substring(0, code.length - 1)) ?? 0; - } - } - - static int _cardCompare(String a, String b) { - final sa = _suitOrder[a[a.length - 1]] ?? 0; - final sb = _suitOrder[b[b.length - 1]] ?? 0; - if (sa != sb) return sa - sb; - 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, - ); + // ناحیه‌ی وسط میز که رهاکردنِ کارت در آن یعنی بازی (بالاتر از دستِ شما). + 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); + // مرتب‌سازی بر اساس خال و رتبه تا انتخاب برای بازیکن راحت‌تر باشد. + final codes = List.from(s.yourHand)..sort(compareCards); final n = codes.length; // کارت‌هایی که دیگر در دست نیستند و به trick هم نرفته‌اند ⇒ حذف. @@ -204,8 +174,7 @@ class HokmGame extends FlameGame { } // کارت‌های دستِ خودتان بزرگ‌تر از کارت‌های روی میز. - final hw = size.x * 0.225; - final hh = hw * kCardRatio; + final hw = size.x * 0.225, 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; @@ -215,23 +184,17 @@ class HokmGame extends FlameGame { final code = codes[i]; final target = Vector2(startX + i * step, y); final legal = _legal(code); - final dim = s.phase == 'playing' && s.isMyTurn && !legal; var c = _hand[code]; if (c == null) { // کارت جدید ⇒ از مرکز میز پخش می‌شود. - c = CardComponent( - code: code, - faceUp: true, - size: Vector2(hw, hh), - position: size / 2, - ); + c = CardComponent(code: code, faceUp: true, size: Vector2(hw, hh), position: size / 2); _hand[code] = c; add(c); } else { c.size = Vector2(hw, hh); } c.onPlay = legal ? () => cubit.playCard(code) : null; - c.dimmed = dim; + c.dimmed = s.phase == 'playing' && s.isMyTurn && !legal; c.home = target; c.dropZone = _dropZone(); c.restPriority = 10 + i; @@ -243,18 +206,16 @@ class HokmGame extends FlameGame { void _layoutTrick(GameState s) { final present = s.trick.map((t) => t.card).toSet(); - // کارت‌های trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده (نوبت بعدی) برو و حذف شو. + // کارت‌های trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده برو و حذف شو. for (final code in _trick.keys.toList()) { - if (!present.contains(code)) { - final c = _trick.remove(code)!; - final winnerOrigin = _seatOrigin(_rel(s.turn)); - c.children.whereType().toList().forEach((e) => e.removeFromParent()); - c.add(SequenceEffect([ - MoveToEffect(winnerOrigin, - EffectController(duration: 0.25, curve: Curves.easeIn)), - RemoveEffect(), - ])); - } + if (present.contains(code)) continue; + final c = _trick.remove(code)!; + c.children.whereType().toList().forEach((e) => e.removeFromParent()); + c.add(SequenceEffect([ + MoveToEffect(_seatOrigin(_rel(s.turn)), + EffectController(duration: 0.25, curve: Curves.easeIn)), + RemoveEffect(), + ])); } for (final tc in s.trick) { @@ -284,6 +245,7 @@ class HokmGame extends FlameGame { } } + // مبدأِ نشستنِ هر جایگاه (برای پرتاب/جمعِ کارت‌ها). Vector2 _seatOrigin(int rel) { final c = size / 2; switch (rel) { @@ -298,6 +260,7 @@ class HokmGame extends FlameGame { } } + // جابه‌جاییِ کارتِ هر جایگاه از مرکز، در ناحیه‌ی trick. Vector2 _trickOffset(int rel) { final d = _cardW * 0.62; switch (rel) { @@ -312,6 +275,8 @@ class HokmGame extends FlameGame { } } + // ===== عناصرِ بازساخته‌شونده در هر به‌روزرسانی ===== + void _rebuildBacksAndInfo(GameState s) { for (final c in _backs) { c.removeFromParent(); @@ -322,12 +287,10 @@ class HokmGame extends FlameGame { _backs.clear(); _info.clear(); - // پشت‌کارت حریفان for (var seat = 0; seat < 4; seat++) { if (seat == s.yourSeat) continue; - final rel = _rel(seat); final count = seat < s.handCounts.length ? s.handCounts[seat] : 0; - _addBacks(rel, count); + _addBacks(_rel(seat), count); } _addTricksWon(s); _addScorePucks(s); @@ -335,9 +298,8 @@ class HokmGame extends FlameGame { } // شمارنده ۱: دست‌های برده‌ی این هَند (tricks_won) — دسته‌کارتِ پشت‌رو + عدد. - // با هر دست یکی اضافه می‌شود؛ رسیدن به ۷ یعنی پایان هَند (سرور صفر می‌کند). + // رسیدن به ۷ یعنی پایان هَند (سرور صفر می‌کند). void _addTricksWon(GameState s) { - const gold = Color(0xFFE9B949); for (var team = 0; team < 2; team++) { final won = team < s.tricksWon.length ? s.tricksWon[team] : 0; if (won <= 0) continue; @@ -351,53 +313,29 @@ class HokmGame extends FlameGame { final n = won.clamp(1, 7); final start = base - step * ((n - 1) / 2); for (var i = 0; i < n; i++) { - final c = CardComponent( - code: '', - faceUp: false, - size: Vector2(w, h), - position: start + step * i.toDouble(), - priority: 2, - ); - _backs.add(c); - add(c); + _addBack(start + step * i.toDouble(), Vector2(w, h), priority: 2); } - final badge = TextComponent( - text: '$won', - anchor: Anchor.center, - position: base - Vector2(0, h * 0.7), - priority: 21, - textRenderer: TextPaint( - style: TextStyle( - fontFamily: 'Dana', - color: gold, - fontSize: _cardW * 0.34, - fontWeight: FontWeight.bold, - shadows: const [Shadow(color: Colors.black, blurRadius: 4)], - ), - ), - ); - _info.add(badge); - add(badge); + _addLabel('$won', base - Vector2(0, h * 0.7), _cardW * 0.34, bold: true); } } - // شمارنده ۲: امتیاز بازی (scores = هندهای برده) — دیسک مرکزیِ هر تیم، ۰ تا ۷. - // رسیدن به ۷ یعنی پایان کل بازی. + // شمارنده ۲: امتیاز بازی (scores = هندهای برده) — دیسکِ هر تیم، ۰ تا ۷. + // یکی جلوی شما (پایین)، دیگری جلوی حریف (راست) — نه جلوی یارِ بالا. void _addScorePucks(GameState s) { final radius = size.x * 0.05; 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 * 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; + final puck = ScorePuck(score, radius: radius, position: pos)..priority = 22; _info.add(puck); add(puck); } } + // پشت‌کارت‌های یک حریف به‌صورت بادبزنی. void _addBacks(int rel, int count) { if (count <= 0) return; final n = count.clamp(1, 13); @@ -419,58 +357,29 @@ class HokmGame extends FlameGame { } final start = base - stepV * ((n - 1) / 2); for (var i = 0; i < n; i++) { - final comp = CardComponent( - code: '', - faceUp: false, - size: Vector2(w, h), - position: start + stepV * i.toDouble(), - priority: 1, - ); - _backs.add(comp); - add(comp); + _addBack(start + stepV * i.toDouble(), Vector2(w, h), priority: 1); } } void _addInfo(GameState s) { - void addText(String text, Vector2 pos, double fs, Color color, - {Anchor anchor = Anchor.center, bool bold = false}) { - final t = TextComponent( - text: text, - anchor: anchor, - position: pos, - priority: 20, - textRenderer: TextPaint( - style: TextStyle( - fontFamily: 'Dana', - color: color, - fontSize: fs, - fontWeight: bold ? FontWeight.bold : FontWeight.normal)), - ); - _info.add(t); - add(t); - } - if (s.trump != null) { final (sym, col) = _trumpGlyph(s.trump!); - addText('حکم: $sym', Vector2(size.x * 0.04, size.y * 0.04), size.x * 0.05, - col, anchor: Anchor.topLeft, bold: true); + _addLabel('حکم: $sym', Vector2(size.x * 0.04, size.y * 0.04), size.x * 0.05, + color: col, anchor: Anchor.topLeft, bold: true); } - addText( - '${s.scores.isNotEmpty ? s.scores[0] : 0} - ${s.scores.length > 1 ? s.scores[1] : 0}', - Vector2(size.x / 2, size.y * 0.04), - size.x * 0.05, - const Color(0xFFE9B949), + final a = s.scores.isNotEmpty ? s.scores[0] : 0; + final b = s.scores.length > 1 ? s.scores[1] : 0; + _addLabel('$a - $b', Vector2(size.x / 2, size.y * 0.04), size.x * 0.05, anchor: Anchor.topCenter); for (final p in s.players) { - final rel = _rel(p.seat); - final pos = _seatLabelPos(rel); + final pos = _seatLabelPos(_rel(p.seat)); final isTurn = s.turn == p.seat; - addText( + _addLabel( '${p.name}${p.bot ? ' (ربات)' : ''}${p.connected ? '' : ' …'}', pos, size.x * 0.035, - isTurn ? const Color(0xFFE9B949) : Colors.white70, + color: isTurn ? const Color(0xFFE9B949) : Colors.white70, bold: isTurn, ); if (isTurn) { @@ -486,6 +395,37 @@ class HokmGame extends FlameGame { } } + // ===== کمکی‌های ساختِ عنصر (ثبت در فهرستِ بازساخته‌شونده) ===== + + void _addBack(Vector2 pos, Vector2 sz, {required int priority}) { + final c = CardComponent(code: '', faceUp: false, size: sz, position: pos, priority: priority); + _backs.add(c); + add(c); + } + + void _addLabel(String text, Vector2 pos, double fontSize, + {Color color = const Color(0xFFE9B949), + Anchor anchor = Anchor.center, + bool bold = false}) { + final t = TextComponent( + text: text, + anchor: anchor, + position: pos, + priority: 21, + textRenderer: TextPaint( + style: TextStyle( + fontFamily: 'Dana', + color: color, + fontSize: fontSize, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + shadows: const [Shadow(color: Colors.black, blurRadius: 4)], + ), + ), + ); + _info.add(t); + add(t); + } + Vector2 _seatLabelPos(int rel) { switch (rel) { case 1: @@ -512,116 +452,3 @@ class HokmGame extends FlameGame { } } } - -/// افکت رعد روی زمین هنگام برد با حکم؛ پس از مدت کوتاهی خودش حذف می‌شود. -class _Lightning extends PositionComponent with HasGameReference { - double _life = 0.55; - static const _max = 0.55; - final _rng = math.Random(); - final List> _bolts = []; - - @override - Future onLoad() async { - size = game.size; - final center = Offset(size.x / 2, size.y / 2); - for (var i = 0; i < 3; i++) { - // هر رعد یک خط شکسته از یک نقطه‌ی تصادفیِ کناری به سمت مرکز. - final start = Offset(_rng.nextDouble() * size.x, _rng.nextDouble() * size.y * 0.4); - final pts = [start]; - const segs = 6; - for (var s = 1; s <= segs; s++) { - final t = s / segs; - final base = Offset.lerp(start, center, t)!; - final jitter = (1 - t) * size.x * 0.06; - pts.add(base + Offset((_rng.nextDouble() - 0.5) * jitter, (_rng.nextDouble() - 0.5) * jitter)); - } - _bolts.add(pts); - } - } - - @override - void update(double dt) { - _life -= dt; - if (_life <= 0) removeFromParent(); - } - - @override - void render(Canvas canvas) { - final op = (_life / _max).clamp(0.0, 1.0); - final glow = Paint() - ..color = const Color(0xFFFFE082).withValues(alpha: op * 0.5) - ..style = PaintingStyle.stroke - ..strokeWidth = size.x * 0.02 - ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); - final core = Paint() - ..color = const Color(0xFFFFFDE7).withValues(alpha: op) - ..style = PaintingStyle.stroke - ..strokeWidth = size.x * 0.006 - ..strokeCap = StrokeCap.round; - for (final bolt in _bolts) { - final path = Path()..moveTo(bolt.first.dx, bolt.first.dy); - for (final p in bolt.skip(1)) { - path.lineTo(p.dx, p.dy); - } - canvas.drawPath(path, glow); - canvas.drawPath(path, core); - } - } -} - -/// دیسکِ فلزیِ شمارش دست‌های برده‌ی یک تیم (۰ تا ۷). -class _Puck extends PositionComponent { - final int count; - _Puck(this.count, {required double radius, super.position}) - : super(size: Vector2.all(radius * 2), anchor: Anchor.center); - - @override - void render(Canvas canvas) { - final r = size.x / 2; - final c = Offset(r, r); - canvas.drawCircle(c, r, Paint()..color = const Color(0xFF14110F)); - canvas.drawCircle( - c, - r * 0.92, - Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = r * 0.22 - ..color = const Color(0xFFB8860B), - ); - final tp = TextPainter( - text: TextSpan( - text: '$count', - style: TextStyle( - fontFamily: 'Dana', - color: const Color(0xFFE9B949), - fontSize: r * 1.05, - fontWeight: FontWeight.bold, - ), - ), - textDirection: TextDirection.ltr, - )..layout(); - tp.paint(canvas, c - Offset(tp.width / 2, tp.height / 2)); - } -} - -/// نمدِ سبز میز. -class _Felt extends PositionComponent with HasGameReference { - @override - void render(Canvas canvas) { - final w = game.size.x, h = game.size.y; - final rect = Rect.fromCenter( - center: Offset(w / 2, h / 2), - width: w * 0.86, - height: h * 0.62, - ); - final rrect = RRect.fromRectAndRadius(rect, Radius.circular(h * 0.3)); - canvas.drawRRect(rrect, Paint()..color = const Color(0xFF1B5E20)); - canvas.drawRRect( - rrect, - Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 6 - ..color = const Color(0xFFB8860B), - ); - } -} diff --git a/lib/features/game/flame/table_pieces.dart b/lib/features/game/flame/table_pieces.dart new file mode 100644 index 0000000..8a4c7dd --- /dev/null +++ b/lib/features/game/flame/table_pieces.dart @@ -0,0 +1,124 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +// رنگ‌های مشترکِ میز. +const _gold = Color(0xFFE9B949); +const _goldDark = Color(0xFFB8860B); + +/// نمدِ سبزِ بیضی‌شکلِ میز با حاشیه‌ی طلایی. +class Felt extends PositionComponent with HasGameReference { + @override + void render(Canvas canvas) { + final w = game.size.x, h = game.size.y; + final rect = Rect.fromCenter( + center: Offset(w / 2, h / 2), + width: w * 0.86, + height: h * 0.62, + ); + final rrect = RRect.fromRectAndRadius(rect, Radius.circular(h * 0.3)); + canvas.drawRRect(rrect, Paint()..color = const Color(0xFF1B5E20)); + canvas.drawRRect( + rrect, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 6 + ..color = _goldDark, + ); + } +} + +/// دیسکِ فلزیِ شمارش (امتیاز تیم) با عددِ وسط؛ از ۰ تا ۷. +class ScorePuck extends PositionComponent { + final int count; + ScorePuck(this.count, {required double radius, super.position}) + : super(size: Vector2.all(radius * 2), anchor: Anchor.center); + + @override + void render(Canvas canvas) { + final r = size.x / 2; + final c = Offset(r, r); + canvas.drawCircle(c, r, Paint()..color = const Color(0xFF14110F)); + canvas.drawCircle( + c, + r * 0.92, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = r * 0.22 + ..color = _goldDark, + ); + final tp = TextPainter( + text: TextSpan( + text: '$count', + style: TextStyle( + fontFamily: 'Dana', + color: _gold, + fontSize: r * 1.05, + fontWeight: FontWeight.bold, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + tp.paint(canvas, c - Offset(tp.width / 2, tp.height / 2)); + } +} + +/// افکت رعد روی زمین هنگام «بریدن با حکم»؛ پس از مدت کوتاهی خودش حذف می‌شود. +class Lightning extends PositionComponent with HasGameReference { + static const _max = 0.55; + double _life = _max; + final _rng = math.Random(); + final List> _bolts = []; + + @override + Future onLoad() async { + size = game.size; + final center = Offset(size.x / 2, size.y / 2); + for (var i = 0; i < 3; i++) { + // هر رعد یک خط شکسته از یک نقطه‌ی تصادفیِ بالا به سمت مرکز. + final start = + Offset(_rng.nextDouble() * size.x, _rng.nextDouble() * size.y * 0.4); + final pts = [start]; + const segs = 6; + for (var s = 1; s <= segs; s++) { + final t = s / segs; + final base = Offset.lerp(start, center, t)!; + final jitter = (1 - t) * size.x * 0.06; + pts.add(base + + Offset((_rng.nextDouble() - 0.5) * jitter, + (_rng.nextDouble() - 0.5) * jitter)); + } + _bolts.add(pts); + } + } + + @override + void update(double dt) { + _life -= dt; + if (_life <= 0) removeFromParent(); + } + + @override + void render(Canvas canvas) { + final op = (_life / _max).clamp(0.0, 1.0); + final glow = Paint() + ..color = const Color(0xFFFFE082).withValues(alpha: op * 0.5) + ..style = PaintingStyle.stroke + ..strokeWidth = size.x * 0.02 + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); + final core = Paint() + ..color = const Color(0xFFFFFDE7).withValues(alpha: op) + ..style = PaintingStyle.stroke + ..strokeWidth = size.x * 0.006 + ..strokeCap = StrokeCap.round; + for (final bolt in _bolts) { + final path = Path()..moveTo(bolt.first.dx, bolt.first.dy); + for (final p in bolt.skip(1)) { + path.lineTo(p.dx, p.dy); + } + canvas.drawPath(path, glow); + canvas.drawPath(path, core); + } + } +}