feat: add game graphic

This commit is contained in:
2026-07-08 09:06:32 +03:30
parent ffd2646eea
commit 46a649166b
15 changed files with 521 additions and 57 deletions
@@ -4,6 +4,7 @@ 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).
@@ -19,6 +20,7 @@ class Felt extends PositionComponent with HasGameReference {
Felt({this.carpet = 'classic'});
Sprite? _carpetSprite;
Sprite? _bgSprite; // پس‌زمینه‌ی نقاشی‌شده (اختیاری، قابلِ تعویض)
bool get _hasCarpet => carpet != 'classic';
@@ -27,6 +29,15 @@ class Felt extends PositionComponent with HasGameReference {
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
@@ -34,18 +45,36 @@ class Felt extends PositionComponent with HasGameReference {
final w = game.size.x, h = game.size.y;
final center = Offset(w / 2, h / 2);
// ۱) زمینِ لاجوردیِ نمدی (کلِ صفحه) با وینیتِ شعاعی برای عمق. همیشه کشیده
// می‌شود تا زیرِ میز/فرش یکدست باشد و لبه‌های فرش با پس‌زمینه جور شود.
canvas.drawRect(
Rect.fromLTWH(0, 0, w, h),
Paint()
..shader = RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: const [AppColors.lapisHi, AppColors.lapisNight],
stops: const [0.5, 1.0],
).createShader(Rect.fromLTWH(0, 0, w, h)),
);
// ۱) پس‌زمینه: تصویرِ سفارشیِ نقاشی‌شده (اگر موجود باشد) یا گرادیانِ لاجوردیِ
// پیش‌فرض. تصویر با پوششِ کامل (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(
@@ -123,6 +152,78 @@ class Felt extends PositionComponent with HasGameReference {
..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,
});
}
/// دیسکِ فلزیِ شمارش (امتیاز تیم) با عددِ وسط؛ از ۰ تا ۷.
@@ -161,6 +262,71 @@ class ScorePuck extends PositionComponent {
}
/// افکت رعد روی زمین هنگام «بریدن با حکم»؛ پس از مدت کوتاهی خودش حذف می‌شود.
/// افکتِ بریدن با حکم: فلاشِ کوتاهِ صفحه + حلقه‌ی شوک‌ویوِ طلایی + جرقه‌ها.
/// همراهِ 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;