import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import '../../../../../core/network/card_images.dart'; import '../../../../../core/settings/app_settings.dart'; import '../../../../../core/theme/app_theme.dart'; // رنگ‌های مشترکِ میز (منبعِ واحد در AppColors). const _gold = AppColors.gold; const _goldDark = AppColors.goldDark; /// میز: زمینِ لاجوردیِ نمدی در کلِ صفحه + یک «میزِ» مرکزی با قابِ طلاییِ دولایه. /// اگر فرشی انتخاب شده باشد، سطحِ میز تصویرِ فرش است (بریده‌شده در شکلِ میز)؛ /// وگرنه سطحِ سرمه‌ایِ ترسیمی. کارت‌ها داخلِ همین ناحیه انداخته می‌شوند. class Felt extends PositionComponent with HasGameReference { /// شناسه‌ی فرشِ انتخابی؛ 'classic' یعنی سطحِ ترسیمی (بدون تصویر). final String carpet; Felt({this.carpet = 'classic'}); Sprite? _carpetSprite; Sprite? _bgSprite; // پس‌زمینه‌ی نقاشی‌شده (اختیاری، قابلِ تعویض) bool get _hasCarpet => carpet != 'classic'; @override Future onLoad() async { if (_hasCarpet) { _carpetSprite = await CardImages.carpet(carpet); } // پس‌زمینه‌ی سفارشی (تالار/کاخ): فایلِ assets/images/game_bg.jpg را بگذارید. // اگر نبود یا حالتِ کم‌مصرف باشد، به گرادیانِ لاجوردیِ پیش‌فرض برمی‌گردد. if (!AppSettings.I.lowGraphics.value) { try { _bgSprite = await Sprite.load('game_bg.jpg'); } catch (_) { _bgSprite = null; } } } @override void render(Canvas canvas) { final w = game.size.x, h = game.size.y; final center = Offset(w / 2, h / 2); // ۱) پس‌زمینه: تصویرِ سفارشیِ نقاشی‌شده (اگر موجود باشد) یا گرادیانِ لاجوردیِ // پیش‌فرض. تصویر با پوششِ کامل (cover) کشیده و کمی تیره می‌شود تا کارت‌ها خوانا // بمانند و وینیتِ شعاعی هم رویش می‌آید تا لبه‌ها با تم جور شود. final full = Rect.fromLTWH(0, 0, w, h); final bg = _bgSprite; if (bg != null) { _drawCover(canvas, bg, w, h); canvas.drawRect(full, Paint()..color = const Color(0x66081428)); canvas.drawRect( full, Paint() ..shader = RadialGradient( center: Alignment.center, radius: 1.1, colors: const [Color(0x00000000), AppColors.lapisNight], stops: const [0.55, 1.0], ).createShader(full), ); } else { canvas.drawRect( full, Paint() ..shader = RadialGradient( center: Alignment.center, radius: 1.0, colors: const [AppColors.lapisHi, AppColors.lapisNight], stops: const [0.5, 1.0], ).createShader(full), ); } // ۲) میزِ مرکزی با قابِ طلایی. final rect = Rect.fromCenter( center: center, width: w * 0.72, height: h * 0.46); final radius = Radius.circular(h * 0.05); final table = RRect.fromRectAndRadius(rect, radius); // سایه‌ی نرمِ زیرِ میز (یک‌بار، نه هر-فریمی سنگین). canvas.drawRRect( table.shift(const Offset(0, 6)), Paint() ..color = const Color(0x66000000) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12), ); // قابِ طلاییِ بیرونی. canvas.drawRRect( RRect.fromRectAndRadius(rect.inflate(w * 0.012), radius), Paint() ..shader = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [_gold, _goldDark], ).createShader(rect), ); // سطحِ میز: تصویرِ فرش (بریده در شکلِ میز) یا سطحِ سرمه‌ایِ ترسیمی. final sprite = _carpetSprite; if (_hasCarpet && sprite != null) { canvas.save(); canvas.clipRRect(table); // فرش را به‌صورتِ cover داخلِ مستطیلِ میز می‌کشیم. sprite.render( canvas, position: Vector2(rect.left, rect.top), size: Vector2(rect.width, rect.height), ); // تیره‌ترِ ملایم برای خوانایی کارت‌ها روی فرش. canvas.drawRRect(table, Paint()..color = const Color(0x1A000000)); canvas.restore(); } else { canvas.drawRRect( table, Paint() ..shader = RadialGradient( center: Alignment.center, radius: 0.9, colors: const [AppColors.lapisLo, AppColors.lapisInk], stops: const [0.4, 1.0], ).createShader(rect), ); } // نورِ نرمِ مرکزی (اسپاتلایت) برای برجسته‌شدنِ کارت‌های وسطِ میز. canvas.save(); canvas.clipRRect(table); canvas.drawRect( rect, Paint() ..shader = RadialGradient( center: Alignment.center, radius: 0.62, colors: const [Color(0x1FFFFFFF), Color(0x00FFFFFF)], stops: const [0.0, 1.0], ).createShader(rect), ); canvas.restore(); // خطِ طلاییِ نازکِ داخلی (قابِ دولایه). canvas.drawRRect( RRect.fromRectAndRadius(rect.deflate(w * 0.018), Radius.circular(h * 0.04)), Paint() ..style = PaintingStyle.stroke ..strokeWidth = w * 0.004 ..color = _gold.withValues(alpha: 0.65), ); } // تصویرِ پس‌زمینه را با حفظِ نسبت، پوششِ کاملِ صفحه (cover) و مرکز-برش می‌کشد. void _drawCover(Canvas canvas, Sprite sprite, double w, double h) { final img = sprite.image; final iw = img.width.toDouble(), ih = img.height.toDouble(); final scale = math.max(w / iw, h / ih); final dw = iw * scale, dh = ih * scale; sprite.render( canvas, position: Vector2((w - dw) / 2, (h - dh) / 2), size: Vector2(dw, dh), ); } } /// ذراتِ طلاییِ معلق (اتمسفرِ سبک). چند نقطه‌ی کم‌رنگ که آرام بالا می‌روند و /// چشمک می‌زنند؛ عمق می‌دهد بدونِ بارِ گرافیکیِ محسوس (بدونِ blur). class Motes extends PositionComponent with HasGameReference { final int count; Motes({this.count = 12}) { priority = 1; // بالای Felt، زیرِ کارت‌ها (priority ۵) } final _rng = math.Random(); double _t = 0; late final List<_Mote> _motes; @override Future onLoad() async { _motes = List.generate( count, (_) => _Mote( x: _rng.nextDouble(), y: _rng.nextDouble(), r: 1.0 + _rng.nextDouble() * 1.8, speed: 0.006 + _rng.nextDouble() * 0.014, drift: (_rng.nextDouble() - 0.5) * 0.03, phase: _rng.nextDouble() * 6.28, ), ); } @override void update(double dt) => _t += dt; @override void render(Canvas canvas) { final w = game.size.x, h = game.size.y; for (final m in _motes) { var y = (m.y - m.speed * _t) % 1.0; if (y < 0) y += 1; final xx = (m.x + m.drift * math.sin(_t * 0.3 + m.phase)) * w; final tw = (0.35 + 0.28 * math.sin(_t * 1.4 + m.phase)).clamp(0.0, 1.0); canvas.drawCircle( Offset(xx, y * h), m.r, Paint()..color = _gold.withValues(alpha: tw * 0.45), ); } } } class _Mote { final double x, y, r, speed, drift, phase; _Mote({ required this.x, required this.y, required this.r, required this.speed, required this.drift, required this.phase, }); } /// دیسکِ فلزیِ شمارش (امتیاز تیم) با عددِ وسط؛ از ۰ تا ۷. 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)); } } /// افکت رعد روی زمین هنگام «بریدن با حکم»؛ پس از مدت کوتاهی خودش حذف می‌شود. /// افکتِ بریدن با حکم: فلاشِ کوتاهِ صفحه + حلقه‌ی شوک‌ویوِ طلایی + جرقه‌ها. /// همراهِ Lightning اضافه می‌شود تا بُرِش «قدرتمند» حس شود. class CutBurst extends PositionComponent with HasGameReference { static const _max = 0.5; double _life = _max; final _rng = math.Random(); late final List<_Spark> _sparks; @override Future onLoad() async { size = game.size; _sparks = List.generate(16, (_) { final ang = _rng.nextDouble() * math.pi * 2; final spd = 0.10 + _rng.nextDouble() * 0.17; return _Spark(ang, spd, 1.5 + _rng.nextDouble() * 2.5); }); } @override void update(double dt) { _life -= dt; if (_life <= 0) removeFromParent(); } @override void render(Canvas canvas) { final t = (1 - _life / _max).clamp(0.0, 1.0); // پیشرفت ۰..۱ final center = Offset(size.x / 2, size.y / 2); // ۱) فلاشِ کوتاهِ صفحه (فقط اوایل). final flash = (1 - t * 3).clamp(0.0, 1.0); if (flash > 0) { canvas.drawRect( Rect.fromLTWH(0, 0, size.x, size.y), Paint()..color = const Color(0xFFFFF3C4).withValues(alpha: flash * 0.45), ); } final fade = (1 - t).clamp(0.0, 1.0); // ۲) حلقه‌ی شوک‌ویوِ طلایی که باز می‌شود. canvas.drawCircle( center, t * size.x * 0.55, Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.x * 0.022 * fade ..color = _gold.withValues(alpha: fade * 0.8) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4), ); // ۳) جرقه‌های طلایی که به بیرون می‌پاشند. final sp = Paint()..color = const Color(0xFFFFF3C4).withValues(alpha: fade); for (final s in _sparks) { final d = s.speed * size.x * t; final p = center + Offset(math.cos(s.ang), math.sin(s.ang)) * d; canvas.drawCircle(p, s.r * fade, sp); } } } class _Spark { final double ang, speed, r; _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; 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); } } }