Files
front-hokm/lib/feature/game/presentation/widgets/flame/table_pieces.dart
T
2026-07-10 00:56:58 +03:30

510 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
/// URL کاملِ تصویرِ فرش (از سرور)؛ خالی یعنی سطحِ ترسیمیِ کلاسیک (بدون تصویر).
final String carpetUrl;
Felt({this.carpetUrl = ''});
Sprite? _carpetSprite;
Sprite? _bgSprite; // پس‌زمینه‌ی نقاشی‌شده (اختیاری، قابلِ تعویض)
bool get _hasCarpet => carpetUrl.isNotEmpty;
@override
Future<void> onLoad() async {
if (_hasCarpet) {
_carpetSprite = await CardImages.carpetByUrl(carpetUrl);
}
// پس‌زمینه‌ی سفارشی (تالار/کاخ): فایلِ 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<void> 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<void> 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<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;
final _rng = math.Random();
final List<List<Offset>> _bolts = [];
@override
Future<void> 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 = <Offset>[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);
}
}
}