fix: delay after fire

This commit is contained in:
2026-07-08 19:20:30 +03:30
parent 6b9b9a0f3a
commit 2496580e86
3 changed files with 217 additions and 35 deletions
@@ -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<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());
@@ -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)));
}
@@ -50,6 +50,14 @@ class HokmGame extends FlameGame {
List<String> _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,6 +99,26 @@ class HokmGame extends FlameGame {
_sub = cubit.stream.listen((ui) {
if (ui.state == null) return;
final s = ui.state!;
// در حینِ «مکثِ آتش»، آخرین وضعیت را بافر کن و بعد از مکث اعمالش کن.
if (_cutHold) {
_pendingState = s;
return;
}
_applyState(s);
});
if (cubit.state.state != null) {
_prevTrick = cubit.state.state!.trick.map((t) => t.card).toList();
_prevHandSize = cubit.state.state!.yourHand.length;
_prevHandSig = (cubit.state.state!.yourHand.toList()..sort()).join(',');
_prevTrump = cubit.state.state!.trump;
_s = cubit.state.state;
_relayout();
}
}
/// اعمالِ یک وضعیتِ بازی روی صحنه (صداها/انیمیشن‌ها + چیدمانِ دوباره). در حالتِ
/// عادی مستقیم از استریم صدا زده می‌شود؛ در «مکثِ آتش» پس از پایانِ مکث.
void _applyState(GameState s) {
_checkTrickSounds(s);
// شروعِ دستِ جدید (پخشِ کارت): انیمیشن و صدای بُر زدن.
// دستِ اول تا پایانِ نمایشِ «جستجوی حریف» به تعویق می‌افتد تا انیمیشن
@@ -124,15 +152,6 @@ class HokmGame extends FlameGame {
_prevHandSize = s.yourHand.length;
_s = s;
_relayout();
});
if (cubit.state.state != null) {
_prevTrick = cubit.state.state!.trick.map((t) => t.card).toList();
_prevHandSize = cubit.state.state!.yourHand.length;
_prevHandSig = (cubit.state.state!.yourHand.toList()..sort()).join(',');
_prevTrump = cubit.state.state!.trump;
_s = cubit.state.state;
_relayout();
}
}
@override
@@ -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;
@@ -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<void> 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;