510 lines
17 KiB
Dart
510 lines
17 KiB
Dart
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<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 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);
|
||
}
|
||
}
|
||
}
|