From 2496580e86e388f9d186b92cf049b61744007cb3 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Wed, 8 Jul 2026 19:20:30 +0330 Subject: [PATCH] fix: delay after fire --- .../widgets/flame/card_component.dart | 14 +- .../presentation/widgets/flame/hokm_game.dart | 115 +++++++++++----- .../widgets/flame/table_pieces.dart | 123 ++++++++++++++++++ 3 files changed, 217 insertions(+), 35 deletions(-) diff --git a/lib/feature/game/presentation/widgets/flame/card_component.dart b/lib/feature/game/presentation/widgets/flame/card_component.dart index e296608..f6b38f6 100644 --- a/lib/feature/game/presentation/widgets/flame/card_component.dart +++ b/lib/feature/game/presentation/widgets/flame/card_component.dart @@ -18,6 +18,7 @@ class CardComponent extends PositionComponent VoidCallback? onPlay; // در صورت مجاز بودن، بازیِ این کارت bool dimmed; // کارت غیرمجاز/غیرفعال Vector2? home; // موقعیت اصلی در دست (برای برگشت پس از کشیدنِ ناقص) + double homeAngle = 0; // زاویه‌ی بادبزنیِ کارت در دست (برای برگشت پس از کشیدن) bool highlight = false; // کارتِ برنده‌ی دست ⇒ درخششِ طلایی پیش از جمع‌شدن int restPriority = 0; // ترتیب لایه‌ی اصلی در دست (برای بازگردانی پس از کشیدن) Rect? dropZone; // ناحیه‌ی وسط میز؛ رهاکردن کارت در آن یعنی بازی @@ -72,6 +73,13 @@ class CardComponent extends PositionComponent Vector2.all(s), EffectController(duration: 0.12, curve: Curves.easeOut))); } + // چرخاندنِ نرمِ کارت به زاویه‌ی مشخص (صاف‌کردن هنگام کشیدن، برگشت هنگام رها). + void _rotateTo(double a) { + children.whereType().toList().forEach((e) => e.removeFromParent()); + add(RotateEffect.to( + a, EffectController(duration: 0.12, curve: Curves.easeOut))); + } + /// بازنشانیِ فوریِ مقیاس به اندازه‌ی عادی (هنگام بازی روی میز). void resetScale() { children.whereType().toList().forEach((e) => e.removeFromParent()); @@ -94,12 +102,15 @@ class CardComponent extends PositionComponent _dragging = true; priority = 1000; // روی همه‌ی کارت‌ها _scaleTo(_dragScale); // بزرگ‌نماییِ نرم هنگام انتخاب + _rotateTo(0); // صاف‌شدن: کارت دیگر با زاویه‌ی بادبزنیِ دست کشیده نمی‌شود } @override void onDragUpdate(DragUpdateEvent event) { if (!_dragging) return; - position += event.localDelta; + // canvasDelta (فضای صفحه) نه localDelta: مستقل از چرخش/مقیاسِ خودِ کارت است، + // پس کارت دقیقاً به‌دنبالِ انگشت می‌آید نه روی مسیرِ منحنیِ قوسِ دست. + position += event.canvasDelta; _overZone = _inDropZone(); } @@ -139,6 +150,7 @@ class CardComponent extends PositionComponent void _returnHome() { priority = restPriority; // بازگردانی ترتیب لایه تا روی کارت‌های دیگر نیفتد + _rotateTo(homeAngle); // بازگشت به زاویه‌ی بادبزنیِ دست if (home == null) return; add(MoveToEffect(home!, EffectController(duration: 0.2, curve: Curves.easeOut))); } diff --git a/lib/feature/game/presentation/widgets/flame/hokm_game.dart b/lib/feature/game/presentation/widgets/flame/hokm_game.dart index 38cd311..9a54d21 100644 --- a/lib/feature/game/presentation/widgets/flame/hokm_game.dart +++ b/lib/feature/game/presentation/widgets/flame/hokm_game.dart @@ -50,6 +50,14 @@ class HokmGame extends FlameGame { List _prevTrick = const []; bool _highlightedTrick = false; // آیا کارتِ برنده‌ی دستِ کامل هایلایت شده (یک‌بار) int _bestTrumpRank = 0; // بالاترین رتبه‌ی حکمِ بازی‌شده در دستِ جاری (برای افکتِ بریدن) + int _cutCombo = 0; // شمارِ بریدنِ پیاپیِ سریع (روبُریدن‌ها) برای افکتِ آتش + double _lastCutAt = -999; // زمانِ آخرین بریدن (برای پنجره‌ی کمبو) + // «مکثِ آتش»: پس از افکتِ آتشِ کمبو، صحنه کوتاه فریز می‌شود تا لحظه دیده شود؛ + // وضعیت‌های ورودی بافر می‌شوند و پس از مکث اعمال می‌گردند (کاربر و بات فرقی ندارند). + bool _cutHold = false; + double _cutHoldUntil = 0; + GameState? _pendingState; + static const double _cutHoldSec = 1.0; int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن String _prevHandSig = ''; // امضای دست؛ برای تشخیصِ بُرِ مجدد (تغییرِ ۵ کارت در فازِ حکم) bool _silent = false; // هنگام خروج از میز، دیگر صدایی پخش نشود @@ -91,39 +99,12 @@ class HokmGame extends FlameGame { _sub = cubit.stream.listen((ui) { if (ui.state == null) return; final s = ui.state!; - _checkTrickSounds(s); - // شروعِ دستِ جدید (پخشِ کارت): انیمیشن و صدای بُر زدن. - // دستِ اول تا پایانِ نمایشِ «جستجوی حریف» به تعویق می‌افتد تا انیمیشن - // زیرِ اوورلی پنهان نشود و صدا با تصویر هماهنگ باشد. - if (_prevHandSize == 0 && s.yourHand.isNotEmpty && !_silent) { - if (_introActive) { - _pendingShuffle = true; - } else { - _shuffleNow(); - } + // در حینِ «مکثِ آتش»، آخرین وضعیت را بافر کن و بعد از مکث اعمالش کن. + if (_cutHold) { + _pendingState = s; + return; } - // بُرِ مجدد: هنوز در فازِ حکم هستیم اما ۵ کارتِ حاکم عوض شده ⇒ انیمیشنِ بُر دوباره. - final sig = (s.yourHand.toList()..sort()).join(','); - if (!_silent && - !_introActive && - s.phase == 'choose_trump' && - s.trump == null && - _prevHandSize > 0 && - _prevHandSig.isNotEmpty && - sig != _prevHandSig) { - _shuffleNow(); - } - _prevHandSig = sig; - // لحظه‌ی انتخابِ حکم: صدا + نمایشِ بزرگِ حکم در مرکز. - if (_prevTrump == null && s.trump != null && !_silent) { - AppSounds.selectHokm(); - _spawnTrumpReveal(s.trump!); - _dealAllNow(); // پخشِ بقیه‌ی کارت‌ها (بازی تا پایانِ آن قفل است) - } - _prevTrump = s.trump; - _prevHandSize = s.yourHand.length; - _s = s; - _relayout(); + _applyState(s); }); if (cubit.state.state != null) { _prevTrick = cubit.state.state!.trick.map((t) => t.card).toList(); @@ -135,6 +116,44 @@ class HokmGame extends FlameGame { } } + /// اعمالِ یک وضعیتِ بازی روی صحنه (صداها/انیمیشن‌ها + چیدمانِ دوباره). در حالتِ + /// عادی مستقیم از استریم صدا زده می‌شود؛ در «مکثِ آتش» پس از پایانِ مکث. + void _applyState(GameState s) { + _checkTrickSounds(s); + // شروعِ دستِ جدید (پخشِ کارت): انیمیشن و صدای بُر زدن. + // دستِ اول تا پایانِ نمایشِ «جستجوی حریف» به تعویق می‌افتد تا انیمیشن + // زیرِ اوورلی پنهان نشود و صدا با تصویر هماهنگ باشد. + if (_prevHandSize == 0 && s.yourHand.isNotEmpty && !_silent) { + if (_introActive) { + _pendingShuffle = true; + } else { + _shuffleNow(); + } + } + // بُرِ مجدد: هنوز در فازِ حکم هستیم اما ۵ کارتِ حاکم عوض شده ⇒ انیمیشنِ بُر دوباره. + final sig = (s.yourHand.toList()..sort()).join(','); + if (!_silent && + !_introActive && + s.phase == 'choose_trump' && + s.trump == null && + _prevHandSize > 0 && + _prevHandSig.isNotEmpty && + sig != _prevHandSig) { + _shuffleNow(); + } + _prevHandSig = sig; + // لحظه‌ی انتخابِ حکم: صدا + نمایشِ بزرگِ حکم در مرکز. + if (_prevTrump == null && s.trump != null && !_silent) { + AppSounds.selectHokm(); + _spawnTrumpReveal(s.trump!); + _dealAllNow(); // پخشِ بقیه‌ی کارت‌ها (بازی تا پایانِ آن قفل است) + } + _prevTrump = s.trump; + _prevHandSize = s.yourHand.length; + _s = s; + _relayout(); + } + @override void onGameResize(Vector2 size) { super.onGameResize(size); @@ -176,8 +195,20 @@ class HokmGame extends FlameGame { _bestTrumpRank = rank; _shake = 1.0; add(Lightning()); + // کمبوِ بریدن: روبُریدن‌های سریعِ پیاپی (پنجره‌ی ~۶ ثانیه) را می‌شمارد. + _cutCombo = (_t - _lastCutAt < 6.0) ? _cutCombo + 1 : 1; + _lastCutAt = _t; if (!AppSettings.I.lowGraphics.value) { add(CutBurst()); // فلاش + شوک‌ویو + جرقه (کم‌مصرف: خاموش) + // سه بریدنِ پیاپی یا بیشتر ⇒ افکتِ آتش (هرچه کمبو بیشتر، شعله بلندتر). + if (_cutCombo >= 3) { + _shake = 1.4; // تکانِ قوی‌تر برای اوجِ کمبو + add(FireBurst(intensity: _cutCombo)); + // مکثِ کوتاه پس از آتش تا لحظه دیده شود؛ کارتِ بعدی (کاربر یا بات) + // تا پایانِ مکث اعمال نمی‌شود. + _cutHold = true; + _cutHoldUntil = _t + _cutHoldSec; + } } AppSounds.cut(); // بریدن با حکم (شمشیر) HapticFeedback.heavyImpact(); // ضربه‌ی لمسیِ بُرِش با حکم @@ -193,6 +224,8 @@ class HokmGame extends FlameGame { /// قطعِ صداهای بازی هنگام خروج از میز (جلوگیری از پخشِ صدای کارت پس از ترک). void silence() { _silent = true; + _cutHold = false; // مکثِ آتش را لغو کن تا وضعیتِ بافرشده اعمال/رها نشود + _pendingState = null; AppSounds.stopHeartBeat(); } @@ -276,6 +309,19 @@ class HokmGame extends FlameGame { void update(double dt) { super.update(dt); _t += dt; + // پایانِ «مکثِ آتش»: آخرین وضعیتِ بافرشده را اعمال کن (یا صحنه را تازه کن). + if (_cutHold && _t >= _cutHoldUntil) { + _cutHold = false; + final p = _pendingState; + _pendingState = null; + if (!_silent) { + if (p != null) { + _applyState(p); + } else if (_s != null) { + _relayout(); + } + } + } if (_shake > 0) _shake = math.max(0, _shake - dt * 2.2); _updateHeartBeat(dt); } @@ -376,8 +422,8 @@ class HokmGame extends FlameGame { // آیا این کارت در نوبتِ فعلی قابل بازی است (با رعایت follow-suit)؟ bool _legal(String code) { final s = _s!; - // هنگامِ اینترو/بُر زدن/پخشِ کارت‌ها، بازیِ کارت مجاز نیست. - if (_introActive || _shuffling || _dealing) return false; + // هنگامِ اینترو/بُر زدن/پخشِ کارت‌ها یا مکثِ آتش، بازیِ کارت مجاز نیست. + if (_introActive || _shuffling || _dealing || _cutHold) return false; if (s.phase != 'playing' || s.trickDone || !s.isMyTurn) return false; final lead = s.leadSuit; if (lead == null || lead.isEmpty) return true; @@ -471,6 +517,7 @@ class HokmGame extends FlameGame { c.size = Vector2(hw, hh); } c.angle = a; // چرخشِ کارت هم‌راستا با شعاعِ قوس + c.homeAngle = a; // زاویه‌ی مرجع برای بازگشت پس از کشیدنِ ناقص c.onPlay = legal ? () => cubit.playCard(code) : null; c.dimmed = s.phase == 'playing' && s.isMyTurn && !legal; c.home = target; diff --git a/lib/feature/game/presentation/widgets/flame/table_pieces.dart b/lib/feature/game/presentation/widgets/flame/table_pieces.dart index 5a4d5e6..d73f3b9 100644 --- a/lib/feature/game/presentation/widgets/flame/table_pieces.dart +++ b/lib/feature/game/presentation/widgets/flame/table_pieces.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; +import 'dart:ui' as ui; import 'package:flame/components.dart'; import 'package:flutter/material.dart'; @@ -327,6 +328,128 @@ class _Spark { _Spark(this.ang, this.speed, this.r); } +/// افکتِ آتش برای «بریدنِ پیاپیِ حکم» (کمبو: چند روبُریدنِ سریع در یک دست). +/// شعله‌هایی که از مرکزِ میز بالا می‌آیند + ذراتِ اخگرِ صعودی. هر چه شدتِ کمبو +/// بیشتر (تعدادِ بریدن) شعله بلندتر و پرحجم‌تر است. +class FireBurst extends PositionComponent with HasGameReference { + /// شدت (۱..۳+): تعدادِ بریدنِ پیاپی؛ ارتفاع/حجمِ شعله را تعیین می‌کند. + final int intensity; + FireBurst({this.intensity = 3}); + + static const _max = 1.15; + double _life = _max; + double _t = 0; + final _rng = math.Random(); + late final List<_Ember> _embers; + late final List<_Flame> _flames; + + double get _scale => (intensity.clamp(3, 6)) / 3.0; // ۱.۰ در ۳ بریدن، بیشتر با کمبو + + @override + Future onLoad() async { + size = game.size; + final n = (18 * _scale).round(); + _embers = List.generate(n, (_) { + final spread = (_rng.nextDouble() - 0.5) * size.x * 0.34; + final rise = 0.30 + _rng.nextDouble() * 0.55; // کسری از ارتفاعِ صعود + final delay = _rng.nextDouble() * 0.35; + return _Ember(spread, rise, delay, 1.5 + _rng.nextDouble() * 2.8); + }); + final f = (5 * _scale).round(); + _flames = List.generate(f, (i) { + final off = (_rng.nextDouble() - 0.5) * size.x * 0.22; + final w = size.x * (0.07 + _rng.nextDouble() * 0.06); + final h = size.x * (0.18 + _rng.nextDouble() * 0.12) * _scale; + final ph = _rng.nextDouble() * math.pi * 2; + return _Flame(off, w, h, ph); + }); + } + + @override + void update(double dt) { + _t += dt; + _life -= dt; + if (_life <= 0) removeFromParent(); + } + + @override + void render(Canvas canvas) { + final prog = (1 - _life / _max).clamp(0.0, 1.0); + // پاکتِ شدت: سریع بالا می‌آید، آرام محو می‌شود. + final env = prog < 0.2 ? prog / 0.2 : (1 - (prog - 0.2) / 0.8); + final base = Offset(size.x / 2, size.y * 0.56); // کفِ شعله (نزدیکِ مرکزِ میز) + + // ۱) هاله‌ی نارنجیِ گرم پشتِ شعله. + canvas.drawCircle( + base, + size.x * 0.28 * env * _scale, + Paint() + ..color = const Color(0xFFFF7A18).withValues(alpha: 0.22 * env) + ..maskFilter = MaskFilter.blur(BlurStyle.normal, size.x * 0.05), + ); + + // ۲) زبانه‌های شعله (نارنجی→زرد) با لرزشِ افقی. + for (final fl in _flames) { + final sway = math.sin(_t * 9 + fl.phase) * size.x * 0.02; + final cx = base.dx + fl.off + sway; + final h = fl.h * env; + final tip = Offset(cx + sway * 0.6, base.dy - h); + final path = Path() + ..moveTo(cx - fl.w / 2, base.dy) + ..quadraticBezierTo(cx - fl.w * 0.5, base.dy - h * 0.5, tip.dx, tip.dy) + ..quadraticBezierTo(cx + fl.w * 0.5, base.dy - h * 0.5, cx + fl.w / 2, base.dy) + ..close(); + canvas.drawPath( + path, + Paint() + ..shader = _flameShader(base.dy, base.dy - h, env) + ..maskFilter = MaskFilter.blur(BlurStyle.normal, size.x * 0.008), + ); + } + + // ۳) اخگرهای صعودی. + for (final e in _embers) { + final lt = ((prog - e.delay) / (1 - e.delay)).clamp(0.0, 1.0); + if (lt <= 0) continue; + final y = base.dy - lt * size.y * 0.42 * e.rise * _scale; + final x = base.dx + e.spread * lt + math.sin(lt * 8 + e.spread) * 3; + final a = (1 - lt) * env; + canvas.drawCircle( + Offset(x, y), + e.r * (1 - lt * 0.5), + Paint() + ..color = Color.lerp( + const Color(0xFFFFE082), + const Color(0xFFFF5722), + lt, + )!.withValues(alpha: a), + ); + } + } + + // گرادیانِ عمودیِ شعله: کفِ زرد/سفید (y0) → نوکِ نارنجیِ کم‌رنگ (y1). + Shader _flameShader(double y0, double y1, double env) => ui.Gradient.linear( + Offset(0, y0), + Offset(0, y1), + [ + const Color(0xFFFFF3C4).withValues(alpha: 0.95 * env), + const Color(0xFFFFB300).withValues(alpha: 0.85 * env), + const Color(0xFFFF5722).withValues(alpha: 0.15 * env), + ], + const [0.0, 0.45, 1.0], + ); +} + +class _Ember { + final double spread, rise, delay, r; + _Ember(this.spread, this.rise, this.delay, this.r); +} + +class _Flame { + final double off, w, h, phase; + _Flame(this.off, this.w, this.h, this.phase); +} + class Lightning extends PositionComponent with HasGameReference { static const _max = 0.55; double _life = _max;