feat: add characters
This commit is contained in:
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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});
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
@@ -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(
|
||||||
|
f.title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.gold,
|
color: AppColors.gold,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14)),
|
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: [
|
||||||
@@ -630,12 +850,14 @@ BoxDecoration _shopCardDeco({bool highlight = false}) => BoxDecoration(
|
|||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
const BoxShadow(
|
const BoxShadow(
|
||||||
color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)),
|
color: Colors.black54,
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 5),
|
||||||
|
),
|
||||||
if (highlight)
|
if (highlight)
|
||||||
BoxShadow(
|
BoxShadow(color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14),
|
||||||
color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
/// عددِ موجودی با جداکنندهی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر.
|
/// عددِ موجودی با جداکنندهی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر.
|
||||||
String _fmt(int n) {
|
String _fmt(int n) {
|
||||||
@@ -761,7 +983,8 @@ 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:
|
||||||
|
isBonus
|
||||||
? const [AppColors.success, AppColors.successDark]
|
? const [AppColors.success, AppColors.successDark]
|
||||||
: const [Color(0xFFFFD54F), AppColors.goldDark],
|
: const [Color(0xFFFFD54F), AppColors.goldDark],
|
||||||
),
|
),
|
||||||
@@ -915,7 +1138,8 @@ 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 =
|
||||||
|
widget.disabled
|
||||||
? const [Color(0xFF555555), Color(0xFF333333)]
|
? const [Color(0xFF555555), Color(0xFF333333)]
|
||||||
: widget.green
|
: widget.green
|
||||||
? const [AppColors.success, AppColors.successDark]
|
? const [AppColors.success, AppColors.successDark]
|
||||||
@@ -945,7 +1169,8 @@ 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(
|
BoxShadow(
|
||||||
color: colors.last.withValues(alpha: 0.55),
|
color: colors.last.withValues(alpha: 0.55),
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user