Files
front-hokm/lib/feature/game/presentation/widgets/flame/table_pieces.dart
T
2026-07-08 09:06:32 +03:30

387 lines
13 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 '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<void> 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<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 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);
}
}
}