feat: add game graphic
This commit is contained in:
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
|
||||
import '../../../../../core/service/app_sounds.dart';
|
||||
import '../../../../../core/settings/app_settings.dart';
|
||||
import '../../../../../core/theme/app_theme.dart';
|
||||
import '../../../domain/entities/game_entities.dart';
|
||||
import '../../bloc/game_bloc.dart';
|
||||
@@ -84,6 +85,9 @@ class HokmGame extends FlameGame {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(Felt(carpet: carpet));
|
||||
if (!AppSettings.I.lowGraphics.value) {
|
||||
add(Motes()); // ذراتِ طلاییِ معلق برای اتمسفر (در حالتِ کممصرف خاموش)
|
||||
}
|
||||
_sub = cubit.stream.listen((ui) {
|
||||
if (ui.state == null) return;
|
||||
final s = ui.state!;
|
||||
@@ -172,6 +176,9 @@ class HokmGame extends FlameGame {
|
||||
_bestTrumpRank = rank;
|
||||
_shake = 1.0;
|
||||
add(Lightning());
|
||||
if (!AppSettings.I.lowGraphics.value) {
|
||||
add(CutBurst()); // فلاش + شوکویو + جرقه (کممصرف: خاموش)
|
||||
}
|
||||
AppSounds.cut(); // بریدن با حکم (شمشیر)
|
||||
HapticFeedback.heavyImpact(); // ضربهی لمسیِ بُرِش با حکم
|
||||
} else {
|
||||
@@ -379,7 +386,8 @@ class HokmGame extends FlameGame {
|
||||
}
|
||||
|
||||
// حرکتِ نرمِ یک کارت به مقصد، بدون انباشتهشدنِ افکتها.
|
||||
void _moveTo(CardComponent c, Vector2 target, {double dur = 0.38}) {
|
||||
void _moveTo(CardComponent c, Vector2 target,
|
||||
{double dur = 0.38, bool bounce = false}) {
|
||||
c.children.whereType<MoveToEffect>().toList().forEach(
|
||||
(e) => e.removeFromParent(),
|
||||
);
|
||||
@@ -387,10 +395,12 @@ class HokmGame extends FlameGame {
|
||||
c.position = target;
|
||||
return;
|
||||
}
|
||||
// کارتِ تازهنشسته روی میز با کمی overshoot مینشیند (حسِ «جا افتادن»).
|
||||
c.add(
|
||||
MoveToEffect(
|
||||
target,
|
||||
EffectController(duration: dur, curve: Curves.easeOutCubic),
|
||||
EffectController(
|
||||
duration: dur, curve: bounce ? Curves.easeOutBack : Curves.easeOutCubic),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -507,6 +517,7 @@ class HokmGame extends FlameGame {
|
||||
for (final tc in s.trick) {
|
||||
final slot = size / 2 + _trickOffset(_rel(tc.seat));
|
||||
var c = _trick[tc.card];
|
||||
final isNew = c == null; // فقط کارتِ تازهنشسته bounce میشود
|
||||
if (c == null) {
|
||||
// اگر خودِ شما بازی کردید، همان کارتِ دست را منتقل کن (پرواز به وسط).
|
||||
c = _hand.remove(tc.card);
|
||||
@@ -530,7 +541,7 @@ class HokmGame extends FlameGame {
|
||||
c.size = Vector2(_cardW, _cardH);
|
||||
c.angle = 0; // کارتهای روی زمین صافاند (چرخشِ بادبزنیِ دست حذف میشود)
|
||||
c.priority = 5;
|
||||
_moveTo(c, slot);
|
||||
_moveTo(c, slot, bounce: isNew);
|
||||
}
|
||||
|
||||
// دستِ کامل (۴ کارت) ⇒ کارتِ برنده را طلایی هایلایت کن و یکبار «پاپ» بزن،
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -211,6 +211,22 @@ class TableHud extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// گرادیانِ قابِ آواتار بر اساسِ نشانِ رتبه (برنز→پادشاه). پیشفرض: طلایی.
|
||||
List<Color> _rankFrame(String tier) {
|
||||
switch (tier) {
|
||||
case 'bronze':
|
||||
return const [Color(0xFFCD9B6A), Color(0xFF7A4A22)];
|
||||
case 'silver':
|
||||
return const [Color(0xFFEDF1F4), Color(0xFF98A2AD)];
|
||||
case 'diamond':
|
||||
return const [Color(0xFF9BE7F0), Color(0xFF2E8BA0)];
|
||||
case 'king':
|
||||
return const [Color(0xFFCE9BEA), Color(0xFF6A2FA0)];
|
||||
default: // gold و ناشناخته/بات
|
||||
return const [Color(0xFFF3D27A), AppColors.goldDark];
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerSeat extends StatelessWidget {
|
||||
final GamePlayer player;
|
||||
final double diameter;
|
||||
@@ -271,19 +287,24 @@ class _PlayerSeat extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// قابِ طلایی + آواتار.
|
||||
// قابِ رتبهای + آواتار (رنگِ قاب بر اساسِ نشانِ رتبهی بازیکن).
|
||||
Container(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFF3D27A), AppColors.goldDark],
|
||||
colors: _rankFrame(player.bot ? '' : player.rankTier),
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 6),
|
||||
boxShadow: [
|
||||
const BoxShadow(color: Colors.black54, blurRadius: 6),
|
||||
if (isTurn)
|
||||
BoxShadow(
|
||||
color: _rankFrame(player.rankTier)[0]
|
||||
.withValues(alpha: 0.6),
|
||||
blurRadius: 12),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.all(ringW),
|
||||
|
||||
Reference in New Issue
Block a user