feat: add game graphic
This commit is contained in:
@@ -13,6 +13,7 @@ import '../bloc/wallet_bloc.dart';
|
||||
import '../bloc/wallet_event.dart';
|
||||
import '../bloc/wallet_state.dart';
|
||||
import '../bloc/wallet_status.dart';
|
||||
import '../widgets/coin_fly.dart';
|
||||
import '../widgets/streak_dialog.dart';
|
||||
|
||||
/// لابی اصلی: کیفپول، دکمه بازی/دورهمی/فروشگاه/سکه روزانه.
|
||||
@@ -24,6 +25,9 @@ class LobbyScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LobbyScreenState extends State<LobbyScreen> {
|
||||
final _coinKey = GlobalKey(); // چیپِ سکه (مقصدِ پروازِ سکهها)
|
||||
final _dailyKey = GlobalKey(); // کارتِ هدیهی روزانه (مبدأ)
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -32,6 +36,20 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
||||
|
||||
void _reload() => context.read<WalletBloc>().add(LoadWalletEvent());
|
||||
|
||||
Offset? _center(GlobalKey k) {
|
||||
final box = k.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null || !box.attached) return null;
|
||||
return box.localToGlobal(box.size.center(Offset.zero));
|
||||
}
|
||||
|
||||
void _flyCoins() {
|
||||
final from = _center(_dailyKey);
|
||||
final to = _center(_coinKey);
|
||||
if (from != null && to != null && mounted) {
|
||||
CoinFly.play(context, from: from, to: to);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -41,6 +59,7 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
||||
listener: (context, state) {
|
||||
final d = state.dailyStatus;
|
||||
if (d is DailySuccess) {
|
||||
_flyCoins(); // پروازِ سکهها از کارتِ هدیه به چیپِ سکه
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('سکه روزانه دریافت شد: +${d.amount}')),
|
||||
);
|
||||
@@ -56,7 +75,10 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_TopBar(wallet: wallet, onCoinTap: () => _openShop(context)),
|
||||
_TopBar(
|
||||
wallet: wallet,
|
||||
coinKey: _coinKey,
|
||||
onCoinTap: () => _openShop(context)),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 8),
|
||||
@@ -121,6 +143,7 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_DailyCard(
|
||||
key: _dailyKey,
|
||||
wallet: wallet,
|
||||
onTap:
|
||||
() => StreakDialog.show(
|
||||
@@ -347,7 +370,7 @@ class _ActionTile extends StatelessWidget {
|
||||
class _DailyCard extends StatelessWidget {
|
||||
final WalletEntity? wallet;
|
||||
final VoidCallback onTap;
|
||||
const _DailyCard({required this.wallet, required this.onTap});
|
||||
const _DailyCard({super.key, required this.wallet, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -478,7 +501,8 @@ class _DailyCard extends StatelessWidget {
|
||||
class _TopBar extends StatelessWidget {
|
||||
final WalletEntity? wallet;
|
||||
final VoidCallback onCoinTap;
|
||||
const _TopBar({required this.wallet, required this.onCoinTap});
|
||||
final Key? coinKey;
|
||||
const _TopBar({required this.wallet, required this.onCoinTap, this.coinKey});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -582,6 +606,7 @@ class _TopBar extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
key: coinKey,
|
||||
onTap: onCoinTap,
|
||||
child: StatChip(
|
||||
icon: Icons.monetization_on,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
|
||||
/// انیمیشنِ «پروازِ سکهها به کیفپول»: چند سکه از مبدأ (مثلاً کارتِ هدیه) روی یک
|
||||
/// قوس به سمتِ مقصد (چیپِ سکه) پرواز میکنند. با Overlay نمایش داده میشود.
|
||||
class CoinFly {
|
||||
static void play(
|
||||
BuildContext context, {
|
||||
required Offset from,
|
||||
required Offset to,
|
||||
int count = 9,
|
||||
}) {
|
||||
final overlay = Overlay.maybeOf(context, rootOverlay: true);
|
||||
if (overlay == null) return;
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (_) => _CoinFlyOverlay(
|
||||
from: from,
|
||||
to: to,
|
||||
count: count,
|
||||
onDone: () => entry.remove(),
|
||||
),
|
||||
);
|
||||
overlay.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
class _CoinFlyOverlay extends StatefulWidget {
|
||||
final Offset from, to;
|
||||
final int count;
|
||||
final VoidCallback onDone;
|
||||
const _CoinFlyOverlay({
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.count,
|
||||
required this.onDone,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CoinFlyOverlay> createState() => _CoinFlyOverlayState();
|
||||
}
|
||||
|
||||
class _CoinFlyOverlayState extends State<_CoinFlyOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _c;
|
||||
late final List<_Coin> _coins;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final r = math.Random();
|
||||
_coins = List.generate(widget.count, (_) {
|
||||
return _Coin(
|
||||
jitter: Offset((r.nextDouble() - 0.5) * 46, (r.nextDouble() - 0.5) * 46),
|
||||
delay: r.nextDouble() * 0.28,
|
||||
arc: -60 - r.nextDouble() * 90, // ارتفاعِ قوس (به بالا)
|
||||
size: 16 + r.nextDouble() * 8,
|
||||
);
|
||||
});
|
||||
_c = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 950),
|
||||
)
|
||||
..addStatusListener((s) {
|
||||
if (s == AnimationStatus.completed) widget.onDone();
|
||||
})
|
||||
..forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_c.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: AnimatedBuilder(
|
||||
animation: _c,
|
||||
builder: (_, __) {
|
||||
return Stack(
|
||||
children: [
|
||||
for (final coin in _coins) _buildCoin(coin),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoin(_Coin coin) {
|
||||
final local = ((_c.value - coin.delay) / (1 - coin.delay)).clamp(0.0, 1.0);
|
||||
if (local <= 0) return const SizedBox.shrink();
|
||||
final start = widget.from + coin.jitter;
|
||||
final end = widget.to;
|
||||
// قوسِ درجهدو: نقطهی کنترل بالای میانه.
|
||||
final mid = Offset((start.dx + end.dx) / 2, (start.dy + end.dy) / 2 + coin.arc);
|
||||
final t = Curves.easeInCubic.transform(local);
|
||||
final p = _quad(start, mid, end, t);
|
||||
final opacity = local < 0.85 ? 1.0 : (1 - (local - 0.85) / 0.15);
|
||||
final scale = 1.0 - 0.35 * local;
|
||||
return Positioned(
|
||||
left: p.dx - coin.size / 2,
|
||||
top: p.dy - coin.size / 2,
|
||||
child: Opacity(
|
||||
opacity: opacity.clamp(0.0, 1.0),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: Icon(Icons.monetization_on,
|
||||
color: AppColors.gold, size: coin.size),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Offset _quad(Offset a, Offset b, Offset c, double t) {
|
||||
final u = 1 - t;
|
||||
return a * (u * u) + b * (2 * u * t) + c * (t * t);
|
||||
}
|
||||
}
|
||||
|
||||
class _Coin {
|
||||
final Offset jitter;
|
||||
final double delay, arc, size;
|
||||
_Coin({
|
||||
required this.jitter,
|
||||
required this.delay,
|
||||
required this.arc,
|
||||
required this.size,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user