feat: add characters

This commit is contained in:
2026-07-10 00:56:58 +03:30
parent 7c067d36f2
commit 97d156c492
28 changed files with 486 additions and 94 deletions
+2
View File
@@ -31,6 +31,7 @@ import '../../feature/ranked/domain/repository/ranked_repository.dart';
import '../../feature/ranked/domain/use_cases/get_leaderboard_usecase.dart'; import '../../feature/ranked/domain/use_cases/get_leaderboard_usecase.dart';
import '../../feature/ranked/presentation/bloc/leaderboard_bloc.dart'; import '../../feature/ranked/presentation/bloc/leaderboard_bloc.dart';
import '../../feature/shop/data/data_source/remote/shop_api_provider.dart'; import '../../feature/shop/data/data_source/remote/shop_api_provider.dart';
import '../../feature/shop/data/data_source/remote/avatar_api_provider.dart';
import '../../feature/shop/data/data_source/remote/carpet_api_provider.dart'; import '../../feature/shop/data/data_source/remote/carpet_api_provider.dart';
import '../../feature/shop/data/data_source/remote/frame_api_provider.dart'; import '../../feature/shop/data/data_source/remote/frame_api_provider.dart';
import '../../feature/tournament/data/data_source/remote/tournament_api_provider.dart'; import '../../feature/tournament/data/data_source/remote/tournament_api_provider.dart';
@@ -71,6 +72,7 @@ Future<void> setupLocator() async {
locator.registerSingleton<ShopApiProvider>(ShopApiProvider()); locator.registerSingleton<ShopApiProvider>(ShopApiProvider());
locator.registerSingleton<CarpetApiProvider>(CarpetApiProvider()); locator.registerSingleton<CarpetApiProvider>(CarpetApiProvider());
locator.registerSingleton<FrameApiProvider>(FrameApiProvider()); locator.registerSingleton<FrameApiProvider>(FrameApiProvider());
locator.registerSingleton<AvatarApiProvider>(AvatarApiProvider());
locator.registerSingleton<ProfileApiProvider>(ProfileApiProvider()); locator.registerSingleton<ProfileApiProvider>(ProfileApiProvider());
locator.registerSingleton<RankedApiProvider>(RankedApiProvider()); locator.registerSingleton<RankedApiProvider>(RankedApiProvider());
locator.registerSingleton<ChatApiProvider>(ChatApiProvider()); locator.registerSingleton<ChatApiProvider>(ChatApiProvider());
+6 -6
View File
@@ -57,17 +57,17 @@ class CardImages {
} }
} }
/// تصویرِ فرش (برای سطحِ میز) از backend: `/carpets/<id>.jpg`. کش‌شده. /// تصویرِ فرش (برای سطحِ میز) از URL کاملِ backend. کش‌شده با کلیدِ URL.
static Future<Sprite?> carpet(String id) { static Future<Sprite?> carpetByUrl(String url) {
return _cache.putIfAbsent('carpet/$id', () async { if (url.isEmpty) return Future.value(null);
return _cache.putIfAbsent('carpet/$url', () async {
try { try {
final res = await _dio final res = await _dio.get<List<int>>(url);
.get<List<int>>('${AppConfig.baseUrl}/carpets/$id.jpg');
final data = res.data; final data = res.data;
if (data == null || data.isEmpty) return null; if (data == null || data.isEmpty) return null;
return Sprite(await _decode(Uint8List.fromList(data))); return Sprite(await _decode(Uint8List.fromList(data)));
} catch (e) { } catch (e) {
debugPrint('CardImages: carpet/$id failed: $e'); debugPrint('CardImages: carpet $url failed: $e');
return null; return null;
} }
}); });
+51
View File
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:random_avatar/random_avatar.dart';
/// آواتارِ یک بازیکن در کلِ بازی: اگر شخصیتِ آواتاری خریده و انتخاب کرده باشد
/// ([imageUrl] یک URL کاملِ svg/png از سرور) همان را نشان می‌دهد؛ وگرنه به آواتارِ
/// تصادفیِ پیش‌فرض بر اساسِ [seed] برمی‌گردد.
///
/// URLِ تصویر همیشه از backend می‌آید (URL-safe)؛ کلاینت آن را نمی‌سازد. یک منبعِ
/// واحد برای رندرِ آواتار در سرِ میز، پروفایل، لابی و فروشگاه (svg با flutter_svg،
/// png/jpg با Image.network).
class PlayerAvatar extends StatelessWidget {
final String seed; // seed آواتارِ تصادفی (fallback)
final String imageUrl; // URL کاملِ تصویرِ شخصیت (از سرور) — خالی ⇒ تصادفی
final double? size; // اگر null، فضای موجود را پر می‌کند
const PlayerAvatar({
super.key,
required this.seed,
this.imageUrl = '',
this.size,
});
bool get _hasCharacter => imageUrl.isNotEmpty;
@override
Widget build(BuildContext context) {
final Widget child;
if (_hasCharacter) {
child =
imageUrl.toLowerCase().endsWith('.svg')
? SvgPicture.network(
imageUrl,
fit: BoxFit.cover,
placeholderBuilder: (_) => _fallback(),
)
: Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _fallback(),
);
} else {
child = _fallback();
}
final clipped = ClipOval(child: SizedBox.expand(child: child));
if (size == null) return clipped;
return SizedBox(width: size, height: size, child: clipped);
}
Widget _fallback() => RandomAvatar(seed.isEmpty ? 'hakem-1' : seed);
}
@@ -9,6 +9,7 @@ class GamePlayer {
final String rankTier; // bronze..king (خالی برای بات) final String rankTier; // bronze..king (خالی برای بات)
final String avatar; // seed آواتارِ انتخابیِ کاربر (خالی برای بات) final String avatar; // seed آواتارِ انتخابیِ کاربر (خالی برای بات)
final String frame; // قابِ آواتارِ انتخابی (کازمتیک؛ خالی ⇒ قابِ رتبه) final String frame; // قابِ آواتارِ انتخابی (کازمتیک؛ خالی ⇒ قابِ رتبه)
final String avatarImg; // شخصیتِ آواتارِ انتخابی (<id>.<ext>؛ خالی ⇒ تصادفی)
GamePlayer.fromJson(Map<String, dynamic> j) GamePlayer.fromJson(Map<String, dynamic> j)
: seat = (j['seat'] ?? 0) as int, : seat = (j['seat'] ?? 0) as int,
@@ -18,7 +19,8 @@ class GamePlayer {
coins = (j['coins'] ?? 0) as int, coins = (j['coins'] ?? 0) as int,
rankTier = (j['rank_tier'] ?? '') as String, rankTier = (j['rank_tier'] ?? '') as String,
avatar = (j['avatar'] ?? '') as String, avatar = (j['avatar'] ?? '') as String,
frame = (j['frame'] ?? '') as String; frame = (j['frame'] ?? '') as String,
avatarImg = (j['avatar_img'] ?? '') as String;
/// seedِ آواتار برای نمایش: آواتارِ انتخابی، وگرنه نام (fallback برای بات). /// seedِ آواتار برای نمایش: آواتارِ انتخابی، وگرنه نام (fallback برای بات).
String get avatarSeed => avatar.isNotEmpty ? avatar : name; String get avatarSeed => avatar.isNotEmpty ? avatar : name;
@@ -9,7 +9,9 @@ class LobbyPlayer {
final String avatar; // seed آواتارِ انتخابیِ کاربر final String avatar; // seed آواتارِ انتخابیِ کاربر
final bool host; final bool host;
final int seat; // جایگاهِ مطلق (۰..۳) final int seat; // جایگاهِ مطلق (۰..۳)
const LobbyPlayer(this.name, this.avatar, this.host, this.seat); final String avatarImg; // شخصیتِ آواتارِ انتخابی (<id>.<ext>؛ خالی ⇒ تصادفی)
const LobbyPlayer(this.name, this.avatar, this.host, this.seat,
{this.avatarImg = ''});
/// seedِ آواتار برای نمایش: آواتارِ انتخابی، وگرنه نام. /// seedِ آواتار برای نمایش: آواتارِ انتخابی، وگرنه نام.
String get avatarSeed => avatar.isNotEmpty ? avatar : name; String get avatarSeed => avatar.isNotEmpty ? avatar : name;
@@ -42,6 +44,7 @@ class TableLobby {
(e['avatar'] ?? '') as String, (e['avatar'] ?? '') as String,
(e['host'] ?? false) as bool, (e['host'] ?? false) as bool,
(e['seat'] ?? 0) as int, (e['seat'] ?? 0) as int,
avatarImg: (e['avatar_img'] ?? '') as String,
)) ))
.toList(), .toList(),
isHost: (j['host'] ?? false) as bool, isHost: (j['host'] ?? false) as bool,
@@ -11,6 +11,7 @@ import '../../../../core/locator/locator.dart';
import '../../../../core/network/ws_client.dart'; import '../../../../core/network/ws_client.dart';
import '../../../../core/service/app_sounds.dart'; import '../../../../core/service/app_sounds.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/player_avatar.dart';
import '../../../shop/data/data_source/remote/frame_api_provider.dart'; import '../../../shop/data/data_source/remote/frame_api_provider.dart';
import '../../../tournament/data/data_source/remote/tournament_api_provider.dart'; import '../../../tournament/data/data_source/remote/tournament_api_provider.dart';
import '../../../tournament/domain/entities/tournament.dart'; import '../../../tournament/domain/entities/tournament.dart';
@@ -39,7 +40,6 @@ class GameScreen extends StatefulWidget {
class _GameScreenState extends State<GameScreen> { class _GameScreenState extends State<GameScreen> {
late final HokmGame _game; late final HokmGame _game;
String _carpet = 'classic'; // فرشِ انتخابی (برای پس‌زمینه‌ی پشتِ صحنه)
Timer? _introTimer; Timer? _introTimer;
bool _introHidden = false; bool _introHidden = false;
int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱) int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱)
@@ -50,11 +50,12 @@ class _GameScreenState extends State<GameScreen> {
super.initState(); super.initState();
final ws = context.read<WalletBloc>().state.walletStatus; final ws = context.read<WalletBloc>().state.walletStatus;
final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple'; final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple';
_carpet = ws is WalletLoaded ? ws.wallet.selectedCarpet : 'classic'; // URL کاملِ فرش از سرور می‌آید (خالی ⇒ میزِ کلاسیک).
final carpetUrl = ws is WalletLoaded ? ws.wallet.selectedCarpetImg : '';
_game = HokmGame( _game = HokmGame(
context.read<GameBloc>(), context.read<GameBloc>(),
skin: skin.isEmpty ? 'simple' : skin, skin: skin.isEmpty ? 'simple' : skin,
carpet: _carpet.isEmpty ? 'classic' : _carpet, carpetUrl: carpetUrl,
); );
_loadTournament(); _loadTournament();
locator<FrameApiProvider>().warmCache(); // رنگِ قاب‌ها برای HUD (best-effort) locator<FrameApiProvider>().warmCache(); // رنگِ قاب‌ها برای HUD (best-effort)
@@ -253,7 +253,10 @@ class _SearchSlot extends StatelessWidget {
) )
: ClipRRect( : ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: RandomAvatar(p.avatarSeed), child: PlayerAvatar(
seed: p.avatarSeed,
imageUrl: p.avatarImg,
),
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:random_avatar/random_avatar.dart';
import '../../../../core/network/ws_client.dart'; import '../../../../core/network/ws_client.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/game_ui.dart'; import '../../../../core/widgets/game_ui.dart';
import '../../../../core/widgets/player_avatar.dart';
import '../bloc/game_bloc.dart'; import '../bloc/game_bloc.dart';
import '../bloc/game_state.dart'; import '../bloc/game_state.dart';
import 'game_screen.dart'; import 'game_screen.dart';
@@ -253,7 +253,10 @@ class _LobbyViewState extends State<_LobbyView> {
) )
: ClipRRect( : ClipRRect(
borderRadius: BorderRadius.circular(9), borderRadius: BorderRadius.circular(9),
child: RandomAvatar(p.avatarSeed), child: PlayerAvatar(
seed: p.avatarSeed,
imageUrl: p.avatarImg,
),
), ),
), ),
if (!empty) if (!empty)
@@ -68,18 +68,18 @@ class HokmGame extends FlameGame {
// ضربانِ قلبِ نوبتِ کاربر (منطق در TurnHeartbeat جدا شده است). // ضربانِ قلبِ نوبتِ کاربر (منطق در TurnHeartbeat جدا شده است).
final TurnHeartbeat _heart = TurnHeartbeat(); final TurnHeartbeat _heart = TurnHeartbeat();
/// فرشِ انتخابی؛ 'classic' یعنی سطحِ میزِ ترسیمی. فرشِ تصویری به‌عنوانِ سطحِ /// URL کاملِ تصویرِ فرش (از سرور)؛ خالی یعنی میزِ ترسیمیِ کلاسیک. فرشِ تصویری
/// میز (بریده در شکلِ میز) روی زمینِ لاجوردی کشیده می‌شود. /// به‌عنوانِ سطحِ میز (بریده در شکلِ میز) روی زمینِ لاجوردی کشیده می‌شود.
final String carpet; final String carpetUrl;
HokmGame(this.cubit, {this.skin = 'simple', this.carpet = 'classic'}); HokmGame(this.cubit, {this.skin = 'simple', this.carpetUrl = ''});
@override @override
Color backgroundColor() => AppColors.lapisNight; Color backgroundColor() => AppColors.lapisNight;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
add(Felt(carpet: carpet)); add(Felt(carpetUrl: carpetUrl));
if (!AppSettings.I.lowGraphics.value) { if (!AppSettings.I.lowGraphics.value) {
add(Motes()); // ذراتِ طلاییِ معلق برای اتمسفر (در حالتِ کم‌مصرف خاموش) add(Motes()); // ذراتِ طلاییِ معلق برای اتمسفر (در حالتِ کم‌مصرف خاموش)
} }
@@ -16,19 +16,19 @@ const _goldDark = AppColors.goldDark;
/// اگر فرشی انتخاب شده باشد، سطحِ میز تصویرِ فرش است (بریده‌شده در شکلِ میز)؛ /// اگر فرشی انتخاب شده باشد، سطحِ میز تصویرِ فرش است (بریده‌شده در شکلِ میز)؛
/// وگرنه سطحِ سرمه‌ایِ ترسیمی. کارت‌ها داخلِ همین ناحیه انداخته می‌شوند. /// وگرنه سطحِ سرمه‌ایِ ترسیمی. کارت‌ها داخلِ همین ناحیه انداخته می‌شوند.
class Felt extends PositionComponent with HasGameReference { class Felt extends PositionComponent with HasGameReference {
/// شناسه‌ی فرشِ انتخابی؛ 'classic' یعنی سطحِ ترسیمی (بدون تصویر). /// URL کاملِ تصویرِ فرش (از سرور)؛ خالی یعنی سطحِ ترسیمیِ کلاسیک (بدون تصویر).
final String carpet; final String carpetUrl;
Felt({this.carpet = 'classic'}); Felt({this.carpetUrl = ''});
Sprite? _carpetSprite; Sprite? _carpetSprite;
Sprite? _bgSprite; // پس‌زمینه‌ی نقاشی‌شده (اختیاری، قابلِ تعویض) Sprite? _bgSprite; // پس‌زمینه‌ی نقاشی‌شده (اختیاری، قابلِ تعویض)
bool get _hasCarpet => carpet != 'classic'; bool get _hasCarpet => carpetUrl.isNotEmpty;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
if (_hasCarpet) { if (_hasCarpet) {
_carpetSprite = await CardImages.carpet(carpet); _carpetSprite = await CardImages.carpetByUrl(carpetUrl);
} }
// پس‌زمینه‌ی سفارشی (تالار/کاخ): فایلِ assets/images/game_bg.jpg را بگذارید. // پس‌زمینه‌ی سفارشی (تالار/کاخ): فایلِ assets/images/game_bg.jpg را بگذارید.
// اگر نبود یا حالتِ کم‌مصرف باشد، به گرادیانِ لاجوردیِ پیش‌فرض برمی‌گردد. // اگر نبود یا حالتِ کم‌مصرف باشد، به گرادیانِ لاجوردیِ پیش‌فرض برمی‌گردد.
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:random_avatar/random_avatar.dart';
import '../../../../core/network/ws_client.dart'; import '../../../../core/network/ws_client.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/frames.dart'; import '../../../../core/theme/frames.dart';
import '../../../../core/theme/ranks.dart'; import '../../../../core/theme/ranks.dart';
import '../../../../core/widgets/player_avatar.dart';
import '../../domain/entities/game_entities.dart'; import '../../domain/entities/game_entities.dart';
import '../bloc/game_state.dart'; import '../bloc/game_state.dart';
@@ -91,8 +91,12 @@ class _PlayerSeat extends StatelessWidget {
color: Colors.white70, color: Colors.white70,
size: diameter * 0.5, size: diameter * 0.5,
) )
: RandomAvatar( : PlayerAvatar(
player.avatarSeed.isEmpty ? '?' : player.avatarSeed), seed: player.avatarSeed.isEmpty
? '?'
: player.avatarSeed,
imageUrl: player.avatarImg,
),
), ),
), ),
), ),
@@ -13,6 +13,7 @@ class ProfileModel {
return ProfileEntity( return ProfileEntity(
name: (name == null || name.isEmpty) ? 'بازیکن' : name, name: (name == null || name.isEmpty) ? 'بازیکن' : name,
avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar, avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar,
avatarImg: (wallet['selected_avatar_img'] ?? '') as String,
mobile: (user['mobile'] as String?) ?? '', mobile: (user['mobile'] as String?) ?? '',
level: (wallet['level'] ?? 1) as int, level: (wallet['level'] ?? 1) as int,
trophies: (wallet['trophies'] ?? 0) as int, trophies: (wallet['trophies'] ?? 0) as int,
@@ -22,6 +22,7 @@ class ProfileStats {
class ProfileEntity { class ProfileEntity {
final String name; final String name;
final String avatar; final String avatar;
final String avatarImg; // شخصیتِ آواتارِ انتخابی (<id>.<ext>؛ خالی ⇒ تصادفی)
final String mobile; final String mobile;
final int level; final int level;
final int trophies; final int trophies;
@@ -35,6 +36,7 @@ class ProfileEntity {
const ProfileEntity({ const ProfileEntity({
required this.name, required this.name,
required this.avatar, required this.avatar,
this.avatarImg = '',
required this.mobile, required this.mobile,
required this.level, required this.level,
required this.trophies, required this.trophies,
@@ -8,6 +8,7 @@ import '../../../../core/settings/app_settings.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/ranks.dart'; import '../../../../core/theme/ranks.dart';
import '../../../../core/widgets/game_ui.dart'; import '../../../../core/widgets/game_ui.dart';
import '../../../../core/widgets/player_avatar.dart';
import '../../../../core/widgets/rank_badge.dart'; import '../../../../core/widgets/rank_badge.dart';
import '../../../wallet/presentation/bloc/wallet_bloc.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart';
import '../../../wallet/presentation/bloc/wallet_event.dart'; import '../../../wallet/presentation/bloc/wallet_event.dart';
@@ -40,7 +40,11 @@ class _HeroCard extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.bgDark, color: AppColors.bgDark,
), ),
child: RandomAvatar(d.avatar, height: 90, width: 90), child: PlayerAvatar(
seed: d.avatar,
imageUrl: d.avatarImg,
size: 90,
),
), ),
), ),
Positioned( Positioned(
@@ -0,0 +1,44 @@
import '../../../../../core/locator/locator.dart';
import '../../../../../core/network/api_provider_imp.dart';
import '../../../domain/entities/avatar_character.dart';
/// دادهٔ شخصیت‌های آواتار از backend (دریافت، خرید، انتخاب).
class AvatarApiProvider {
ApiProviderImp get _api => locator<ApiProviderImp>();
Future<AvatarCatalog> getAvatars() async {
final res = await _api.get('/avatars');
if (res.statusCode != 200) {
throw Exception('avatars: ${res.statusCode}');
}
final list = (res.data['avatars'] as List?) ?? const [];
final avatars = list
.map((e) => AvatarCharacter.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
return AvatarCatalog(
avatars: avatars,
selected: (res.data['selected'] ?? '') as String,
);
}
/// خرید؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
Future<String?> buyAvatar(String id) => _post('/shop/buy-avatar', id);
/// انتخاب؛ id خالی ⇒ بازگشت به آواتارِ تصادفی. null یعنی موفق.
Future<String?> selectAvatar(String id) => _post('/shop/select-avatar', id);
Future<String?> _post(String path, String id) async {
final res = await _api.post(path, body: {'avatar_id': id});
if (res.statusCode == 200) return null;
switch (res.statusCode) {
case 402:
return 'سکهٔ کافی ندارید';
case 403:
return 'این شخصیت را ندارید';
case 409:
return 'این شخصیت را دارید';
default:
return 'عملیات ناموفق بود';
}
}
}
@@ -0,0 +1,34 @@
/// یک شخصیتِ آواتار (کازمتیک تصویری). owned یعنی قابلِ انتخاب.
class AvatarCharacter {
final String id;
final String title;
final int priceCoins;
final bool vip;
final String img; // نامِ فایل: <id>.<ext> (برای ساختِ URL و انتخابِ رندرر)
final bool owned;
const AvatarCharacter({
required this.id,
required this.title,
required this.priceCoins,
required this.vip,
required this.img,
required this.owned,
});
factory AvatarCharacter.fromJson(Map<String, dynamic> j) => AvatarCharacter(
id: (j['id'] ?? '') as String,
title: (j['title'] ?? '') as String,
priceCoins: (j['price_coins'] ?? 0) as int,
vip: (j['vip'] ?? false) as bool,
img: (j['img'] ?? '') as String,
owned: (j['owned'] ?? false) as bool,
);
}
/// کاتالوگِ شخصیت‌ها + شناسه‌ی شخصیتِ انتخابیِ کاربر (خالی ⇒ آواتارِ تصادفی).
class AvatarCatalog {
final List<AvatarCharacter> avatars;
final String selected;
const AvatarCatalog({required this.avatars, required this.selected});
}
+4 -5
View File
@@ -1,11 +1,10 @@
import '../../../../core/config.dart'; /// یک فرشِ ایرانی که به‌جای میزِ بازی استفاده می‌شود. تصویر (URL کامل) از backend می‌آید.
/// یک فرشِ ایرانی که به‌جای میزِ بازی استفاده می‌شود. تصویر از backend می‌آید.
class Carpet { class Carpet {
final String id; final String id;
final String title; final String title;
final int priceCoins; final int priceCoins;
final bool vip; // برای VIP رایگان final bool vip; // برای VIP رایگان
final String img; // URL کاملِ تصویر (از سرور)؛ خالی برای classic
final bool owned; final bool owned;
const Carpet({ const Carpet({
@@ -13,19 +12,19 @@ class Carpet {
required this.title, required this.title,
required this.priceCoins, required this.priceCoins,
required this.vip, required this.vip,
required this.img,
required this.owned, required this.owned,
}); });
/// 'classic' یعنی میزِ ترسیمیِ پیش‌فرض (بدون تصویر). /// 'classic' یعنی میزِ ترسیمیِ پیش‌فرض (بدون تصویر).
bool get isClassic => id == 'classic'; bool get isClassic => id == 'classic';
String get imageUrl => '${AppConfig.baseUrl}/carpets/$id.jpg';
factory Carpet.fromJson(Map<String, dynamic> j) => Carpet( factory Carpet.fromJson(Map<String, dynamic> j) => Carpet(
id: (j['id'] ?? '') as String, id: (j['id'] ?? '') as String,
title: (j['title'] ?? '') as String, title: (j['title'] ?? '') as String,
priceCoins: (j['price_coins'] ?? 0) as int, priceCoins: (j['price_coins'] ?? 0) as int,
vip: (j['vip'] ?? false) as bool, vip: (j['vip'] ?? false) as bool,
img: (j['img'] ?? '') as String,
owned: (j['owned'] ?? false) as bool, owned: (j['owned'] ?? false) as bool,
); );
} }
@@ -7,12 +7,15 @@ import '../../../../core/locator/locator.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/frames.dart'; import '../../../../core/theme/frames.dart';
import '../../../../core/widgets/game_ui.dart'; import '../../../../core/widgets/game_ui.dart';
import '../../../../core/widgets/player_avatar.dart';
import '../../../wallet/presentation/bloc/wallet_bloc.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart';
import '../../../wallet/presentation/bloc/wallet_event.dart'; import '../../../wallet/presentation/bloc/wallet_event.dart';
import '../../../wallet/presentation/bloc/wallet_state.dart'; import '../../../wallet/presentation/bloc/wallet_state.dart';
import '../../../wallet/presentation/bloc/wallet_status.dart'; import '../../../wallet/presentation/bloc/wallet_status.dart';
import '../../data/data_source/remote/avatar_api_provider.dart';
import '../../data/data_source/remote/carpet_api_provider.dart'; import '../../data/data_source/remote/carpet_api_provider.dart';
import '../../data/data_source/remote/frame_api_provider.dart'; import '../../data/data_source/remote/frame_api_provider.dart';
import '../../domain/entities/avatar_character.dart';
import '../../domain/entities/carpet.dart'; import '../../domain/entities/carpet.dart';
import '../../domain/entities/frame.dart'; import '../../domain/entities/frame.dart';
import '../../domain/entities/shop_entities.dart'; import '../../domain/entities/shop_entities.dart';
@@ -30,7 +33,7 @@ class ShopScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return DefaultTabController(
length: 7, length: 8,
child: Scaffold( child: Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: GameBackground( body: GameBackground(
@@ -95,6 +98,7 @@ class ShopScreen extends StatelessWidget {
_CardsTab(data: d), _CardsTab(data: d),
const _CarpetsTab(), const _CarpetsTab(),
const _FramesTab(), const _FramesTab(),
const _AvatarsTab(),
_BoostersTab(boosters: d.boosters), _BoostersTab(boosters: d.boosters),
_VipTab(packages: d.vipPackages), _VipTab(packages: d.vipPackages),
], ],
@@ -10,6 +10,7 @@ class _ShopTabs extends StatelessWidget {
(Icons.style, 'کارت'), (Icons.style, 'کارت'),
(Icons.casino, 'فرش'), (Icons.casino, 'فرش'),
(Icons.filter_center_focus, 'قاب'), (Icons.filter_center_focus, 'قاب'),
(Icons.face, 'شخصیت'),
(Icons.bolt, 'تجهیزات'), (Icons.bolt, 'تجهیزات'),
(Icons.workspace_premium, 'VIP'), (Icons.workspace_premium, 'VIP'),
]; ];
@@ -352,7 +353,7 @@ class _CarpetCard extends StatelessWidget {
), ),
) )
: Image.network( : Image.network(
c.imageUrl, c.img,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: errorBuilder:
(_, __, ___) => Container( (_, __, ___) => Container(
@@ -430,8 +431,8 @@ class _FramesTabState extends State<_FramesTab> {
} }
void _reload() => setState(() { void _reload() => setState(() {
_future = _api.getFrames(); _future = _api.getFrames();
}); });
Future<void> _act(AvatarFrame f, {required bool buy}) async { Future<void> _act(AvatarFrame f, {required bool buy}) async {
setState(() => _busy = f.id); setState(() => _busy = f.id);
@@ -453,12 +454,15 @@ class _FramesTabState extends State<_FramesTab> {
builder: (context, snap) { builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) { if (snap.connectionState != ConnectionState.done) {
return const Center( return const Center(
child: CircularProgressIndicator(color: AppColors.gold)); child: CircularProgressIndicator(color: AppColors.gold),
);
} }
if (!snap.hasData) { if (!snap.hasData) {
return Center( return Center(
child: TextButton( child: TextButton(
onPressed: _reload, child: const Text('تلاش مجدد')), onPressed: _reload,
child: const Text('تلاش مجدد'),
),
); );
} }
final cat = snap.data!; final cat = snap.data!;
@@ -471,7 +475,8 @@ class _FramesTabState extends State<_FramesTab> {
_FrameCard( _FrameCard(
frame: f, frame: f,
avatarSeed: seed.isEmpty ? 'hakem-1' : seed, avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
selected: cat.selected == f.id || selected:
cat.selected == f.id ||
(cat.selected.isEmpty && f.id == 'none'), (cat.selected.isEmpty && f.id == 'none'),
busy: _busy == f.id, busy: _busy == f.id,
onSelect: () => _act(f, buy: false), onSelect: () => _act(f, buy: false),
@@ -503,19 +508,23 @@ class _FrameCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final f = frame; final f = frame;
final grad = frameGradient(f.id) ?? const [Color(0xFFF3D27A), AppColors.goldDark]; final grad =
frameGradient(f.id) ?? const [Color(0xFFF3D27A), AppColors.goldDark];
return Container( return Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
decoration: _shopCardDeco(highlight: selected), decoration: _shopCardDeco(highlight: selected),
child: Column( child: Column(
children: [ children: [
Text(f.title, Text(
maxLines: 1, f.title,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: const TextStyle( overflow: TextOverflow.ellipsis,
color: AppColors.gold, style: const TextStyle(
fontWeight: FontWeight.bold, color: AppColors.gold,
fontSize: 14)), fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
// پیش‌نمایش: آواتار درونِ قاب با گرادیانِ همان قاب. // پیش‌نمایش: آواتار درونِ قاب با گرادیانِ همان قاب.
Expanded( Expanded(
@@ -536,9 +545,7 @@ class _FrameCard extends StatelessWidget {
), ),
), ),
child: ClipOval( child: ClipOval(
child: SizedBox.expand( child: SizedBox.expand(child: RandomAvatar(avatarSeed)),
child: RandomAvatar(avatarSeed),
),
), ),
); );
}, },
@@ -549,8 +556,10 @@ class _FrameCard extends StatelessWidget {
if (f.vip && !f.owned) if (f.vip && !f.owned)
const Padding( const Padding(
padding: EdgeInsets.only(bottom: 4), padding: EdgeInsets.only(bottom: 4),
child: Text('ویژه‌ی VIP', child: Text(
style: TextStyle(color: AppColors.gold, fontSize: 11)), 'ویژه‌ی VIP',
style: TextStyle(color: AppColors.gold, fontSize: 11),
),
), ),
SizedBox(width: double.infinity, child: _action(f)), SizedBox(width: double.infinity, child: _action(f)),
], ],
@@ -560,8 +569,12 @@ class _FrameCard extends StatelessWidget {
Widget _action(AvatarFrame f) { Widget _action(AvatarFrame f) {
if (busy) return const _PriceButton(label: '...', disabled: true); if (busy) return const _PriceButton(label: '...', disabled: true);
if (selected) return const _PriceButton(label: 'انتخاب شده', disabled: true); if (selected) {
if (f.owned) return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect); return const _PriceButton(label: 'انتخاب شده', disabled: true);
}
if (f.owned) {
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
}
return _PriceButton( return _PriceButton(
label: '${f.priceCoins} سکه', label: '${f.priceCoins} سکه',
green: true, green: true,
@@ -571,6 +584,213 @@ class _FrameCard extends StatelessWidget {
} }
} }
// ===== تبِ شخصیت‌های آواتار =====
class _AvatarsTab extends StatefulWidget {
const _AvatarsTab();
@override
State<_AvatarsTab> createState() => _AvatarsTabState();
}
class _AvatarsTabState extends State<_AvatarsTab> {
final _api = locator<AvatarApiProvider>();
late Future<AvatarCatalog> _future;
String? _busy;
@override
void initState() {
super.initState();
_future = _api.getAvatars();
}
void _reload() => setState(() {
_future = _api.getAvatars();
});
Future<void> _act(AvatarCharacter a, {required bool buy}) async {
setState(() => _busy = a.id);
final err =
buy ? await _api.buyAvatar(a.id) : await _api.selectAvatar(a.id);
if (!mounted) return;
setState(() => _busy = null);
if (err == null) {
context.read<WalletBloc>().add(LoadWalletEvent());
_reload();
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
}
}
Future<void> _clear() async {
setState(() => _busy = '__none__');
await _api.selectAvatar(''); // بازگشت به آواتارِ تصادفی
if (!mounted) return;
setState(() => _busy = null);
context.read<WalletBloc>().add(LoadWalletEvent());
_reload();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<AvatarCatalog>(
future: _future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(
child: CircularProgressIndicator(color: AppColors.gold),
);
}
if (!snap.hasData) {
return Center(
child: TextButton(
onPressed: _reload,
child: const Text('تلاش مجدد'),
),
);
}
final cat = snap.data!;
final ws = context.watch<WalletBloc>().state.walletStatus;
final seed = ws is WalletLoaded ? ws.wallet.avatar : 'hakem-1';
if (cat.avatars.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'به‌زودی شخصیت‌های تازه اضافه می‌شوند.',
style: TextStyle(color: AppColors.gold),
),
),
);
}
return _grid(
note:
'شخصیتِ آواتارت را انتخاب کن؛ در کلِ بازی (سرِ میز، پروفایل و لابی) دیده می‌شود.',
children: [
// «پیش‌فرض»: بازگشت به آواتارِ تصادفی.
_AvatarCard.defaultRandom(
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
selected: cat.selected.isEmpty,
busy: _busy == '__none__',
onSelect: _clear,
),
for (final a in cat.avatars)
_AvatarCard(
avatar: a,
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
selected: cat.selected == a.id,
busy: _busy == a.id,
onSelect: () => _act(a, buy: false),
onBuy: () => _act(a, buy: true),
),
],
);
},
);
}
}
class _AvatarCard extends StatelessWidget {
final AvatarCharacter? avatar; // null ⇒ کارتِ «پیش‌فرض» (آواتارِ تصادفی)
final String avatarSeed;
final bool selected;
final bool busy;
final VoidCallback onSelect;
final VoidCallback? onBuy;
const _AvatarCard({
required this.avatar,
required this.avatarSeed,
required this.selected,
required this.busy,
required this.onSelect,
required this.onBuy,
});
const _AvatarCard.defaultRandom({
required this.avatarSeed,
required this.selected,
required this.busy,
required this.onSelect,
}) : avatar = null,
onBuy = null;
@override
Widget build(BuildContext context) {
final a = avatar;
final title = a?.title ?? 'پیش‌فرض';
return Container(
padding: const EdgeInsets.all(10),
decoration: _shopCardDeco(highlight: selected),
child: Column(
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.gold,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
Expanded(
child: Center(
child: LayoutBuilder(
builder: (_, box) {
final d = box.maxHeight.clamp(40.0, 96.0);
return Container(
width: d,
height: d,
padding: EdgeInsets.all(d * 0.06),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.bgDark,
border: Border.all(
color: selected ? AppColors.gold : AppColors.goldDark,
width: selected ? 2 : 1.4,
),
),
child: PlayerAvatar(
seed: avatarSeed,
imageUrl: a?.img ?? '',
),
);
},
),
),
),
const SizedBox(height: 6),
if (a != null && a.vip && !a.owned)
const Padding(
padding: EdgeInsets.only(bottom: 4),
child: Text(
'ویژه‌ی VIP',
style: TextStyle(color: AppColors.gold, fontSize: 11),
),
),
SizedBox(width: double.infinity, child: _action(a)),
],
),
);
}
Widget _action(AvatarCharacter? a) {
if (busy) return const _PriceButton(label: '...', disabled: true);
if (selected) {
return const _PriceButton(label: 'انتخاب شده', disabled: true);
}
if (a == null || a.owned) {
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
}
return _PriceButton(
label: '${a.priceCoins} سکه',
green: true,
coin: true,
onTap: onBuy,
);
}
}
Widget _grid({required String note, required List<Widget> children}) { Widget _grid({required String note, required List<Widget> children}) {
return Column( return Column(
children: [ children: [
@@ -618,24 +838,26 @@ Widget _grid({required String note, required List<Widget> children}) {
/// قابِ مشترکِ کارت‌های فروشگاه (سکه/کارت/فرش/…): گرادیانِ لاجوردی، حاشیه‌ی طلایی /// قابِ مشترکِ کارت‌های فروشگاه (سکه/کارت/فرش/…): گرادیانِ لاجوردی، حاشیه‌ی طلایی
/// و عمقِ سایه. [highlight] برای آیتمِ انتخاب‌شده هاله‌ی طلایی اضافه می‌کند. /// و عمقِ سایه. [highlight] برای آیتمِ انتخاب‌شده هاله‌ی طلایی اضافه می‌کند.
BoxDecoration _shopCardDeco({bool highlight = false}) => BoxDecoration( BoxDecoration _shopCardDeco({bool highlight = false}) => BoxDecoration(
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF1C4A80), Color(0xFF0B2646)], colors: [Color(0xFF1C4A80), Color(0xFF0B2646)],
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: highlight ? AppColors.gold : AppColors.goldDark, color: highlight ? AppColors.gold : AppColors.goldDark,
width: highlight ? 2 : 1.3, width: highlight ? 2 : 1.3,
), ),
boxShadow: [ boxShadow: [
const BoxShadow( const BoxShadow(
color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)), color: Colors.black54,
if (highlight) blurRadius: 10,
BoxShadow( offset: Offset(0, 5),
color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14), ),
], if (highlight)
); BoxShadow(color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14),
],
);
/// عددِ موجودی با جداکننده‌ی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر. /// عددِ موجودی با جداکننده‌ی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر.
String _fmt(int n) { String _fmt(int n) {
@@ -761,9 +983,10 @@ class _ItemCard extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: isBonus colors:
? const [AppColors.success, AppColors.successDark] isBonus
: const [Color(0xFFFFD54F), AppColors.goldDark], ? const [AppColors.success, AppColors.successDark]
: const [Color(0xFFFFD54F), AppColors.goldDark],
), ),
boxShadow: const [ boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 4), BoxShadow(color: Colors.black54, blurRadius: 4),
@@ -915,14 +1138,15 @@ class _PriceButtonState extends State<_PriceButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final busy = context.select((ShopBloc c) => c.state.busy); final busy = context.select((ShopBloc c) => c.state.busy);
final enabled = !(widget.disabled || busy) && widget.onTap != null; final enabled = !(widget.disabled || busy) && widget.onTap != null;
final colors = widget.disabled final colors =
? const [Color(0xFF555555), Color(0xFF333333)] widget.disabled
: widget.green ? const [Color(0xFF555555), Color(0xFF333333)]
: widget.green
? const [AppColors.success, AppColors.successDark] ? const [AppColors.success, AppColors.successDark]
: const [ : const [
AppColors.accent, AppColors.accent,
Color(0xFF7A1019), Color(0xFF7A1019),
]; // شنگرفِ ایرانی (هم‌رنگِ تم) ]; // شنگرفِ ایرانی (هم‌رنگِ تم)
return GestureDetector( return GestureDetector(
onTapDown: enabled ? (_) => setState(() => _down = true) : null, onTapDown: enabled ? (_) => setState(() => _down = true) : null,
onTapUp: enabled ? (_) => setState(() => _down = false) : null, onTapUp: enabled ? (_) => setState(() => _down = false) : null,
@@ -945,15 +1169,16 @@ class _PriceButtonState extends State<_PriceButton> {
), ),
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
border: Border.all(color: AppColors.gold, width: 1.2), border: Border.all(color: AppColors.gold, width: 1.2),
boxShadow: enabled boxShadow:
? [ enabled
BoxShadow( ? [
color: colors.last.withValues(alpha: 0.55), BoxShadow(
blurRadius: 8, color: colors.last.withValues(alpha: 0.55),
offset: const Offset(0, 3), blurRadius: 8,
), offset: const Offset(0, 3),
] ),
: null, ]
: null,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -13,8 +13,10 @@ class WalletModel extends WalletEntity {
required super.vip, required super.vip,
required super.selectedCard, required super.selectedCard,
required super.selectedCarpet, required super.selectedCarpet,
super.selectedCarpetImg,
required super.name, required super.name,
required super.avatar, required super.avatar,
super.avatarImg,
super.dailyStreak, super.dailyStreak,
super.bestStreak, super.bestStreak,
super.dailyReady, super.dailyReady,
@@ -38,8 +40,10 @@ class WalletModel extends WalletEntity {
vip: (wallet['vip'] ?? false) as bool, vip: (wallet['vip'] ?? false) as bool,
selectedCard: (wallet['selected_card'] ?? 'simple') as String, selectedCard: (wallet['selected_card'] ?? 'simple') as String,
selectedCarpet: (wallet['selected_carpet'] ?? 'classic') as String, selectedCarpet: (wallet['selected_carpet'] ?? 'classic') as String,
selectedCarpetImg: (wallet['selected_carpet_img'] ?? '') as String,
name: (name == null || name.isEmpty) ? 'بازیکن' : name, name: (name == null || name.isEmpty) ? 'بازیکن' : name,
avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar, avatar: (avatar == null || avatar.isEmpty) ? 'hakem-1' : avatar,
avatarImg: (wallet['selected_avatar_img'] ?? '') as String,
dailyStreak: (wallet['daily_streak'] ?? 0) as int, dailyStreak: (wallet['daily_streak'] ?? 0) as int,
bestStreak: (wallet['best_streak'] ?? 0) as int, bestStreak: (wallet['best_streak'] ?? 0) as int,
dailyReady: (wallet['daily_ready'] ?? true) as bool, dailyReady: (wallet['daily_ready'] ?? true) as bool,
@@ -10,8 +10,10 @@ class WalletEntity {
final bool vip; final bool vip;
final String selectedCard; final String selectedCard;
final String selectedCarpet; final String selectedCarpet;
final String selectedCarpetImg; // URL کاملِ تصویرِ فرش (خالی ⇒ میزِ کلاسیک)
final String name; final String name;
final String avatar; final String avatar;
final String avatarImg; // شخصیتِ آواتارِ انتخابی (<id>.<ext>؛ خالی ⇒ تصادفی)
final int dailyStreak; // روزهای پیاپیِ دریافتِ سکه‌ی روزانه final int dailyStreak; // روزهای پیاپیِ دریافتِ سکه‌ی روزانه
final int bestStreak; final int bestStreak;
final bool dailyReady; // آیا امروز سکه‌ی روزانه قابلِ دریافت است final bool dailyReady; // آیا امروز سکه‌ی روزانه قابلِ دریافت است
@@ -28,8 +30,10 @@ class WalletEntity {
required this.vip, required this.vip,
required this.selectedCard, required this.selectedCard,
required this.selectedCarpet, required this.selectedCarpet,
this.selectedCarpetImg = '',
required this.name, required this.name,
required this.avatar, required this.avatar,
this.avatarImg = '',
this.dailyStreak = 0, this.dailyStreak = 0,
this.bestStreak = 0, this.bestStreak = 0,
this.dailyReady = true, this.dailyReady = true,
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:random_avatar/random_avatar.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/game_ui.dart'; import '../../../../core/widgets/game_ui.dart';
import '../../../../core/widgets/player_avatar.dart';
import '../../../auth/presentation/bloc/auth_bloc.dart'; import '../../../auth/presentation/bloc/auth_bloc.dart';
import '../../../auth/presentation/bloc/auth_event.dart'; import '../../../auth/presentation/bloc/auth_event.dart';
import '../../domain/entities/wallet_entity.dart'; import '../../domain/entities/wallet_entity.dart';
@@ -350,7 +350,7 @@ class _TopBar extends StatelessWidget {
child: child:
(w == null || w.avatar.isEmpty) (w == null || w.avatar.isEmpty)
? const Icon(Icons.person, color: AppColors.gold) ? const Icon(Icons.person, color: AppColors.gold)
: ClipOval(child: RandomAvatar(w.avatar)), : PlayerAvatar(seed: w.avatar, imageUrl: w.avatarImg),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
+1 -1
View File
@@ -279,7 +279,7 @@ packages:
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
flutter_svg: flutter_svg:
dependency: transitive dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
+1
View File
@@ -45,6 +45,7 @@ dependencies:
flame: ^1.30.1 flame: ^1.30.1
flame_audio: ^2.11.14 flame_audio: ^2.11.14
random_avatar: ^0.0.8 random_avatar: ^0.0.8
flutter_svg: ^2.3.0 # نمایشِ شخصیتِ آواتارِ svg (و png با Image.network)
# خرید درون‌برنامه‌ای: هر flavor یکی را استفاده می‌کند (مایکت/کافه‌بازار). # خرید درون‌برنامه‌ای: هر flavor یکی را استفاده می‌کند (مایکت/کافه‌بازار).
myket_iap: ^1.1.11 myket_iap: ^1.1.11
flutter_poolakey: ^2.2.0 flutter_poolakey: ^2.2.0