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
+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);
}