feat: add carpet

This commit is contained in:
2026-07-03 21:56:34 +03:30
parent 5160deb973
commit 7acd72c286
21 changed files with 536 additions and 156 deletions
@@ -31,6 +31,7 @@ class GameScreen extends StatefulWidget {
class _GameScreenState extends State<GameScreen> {
late final HokmGame _game;
String _carpet = 'classic'; // فرشِ انتخابی (برای پس‌زمینه‌ی پشتِ صحنه)
Timer? _introTimer;
bool _introHidden = false;
int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱)
@@ -40,9 +41,11 @@ class _GameScreenState extends State<GameScreen> {
super.initState();
final ws = context.read<WalletBloc>().state.walletStatus;
final skin = ws is WalletLoaded ? ws.wallet.selectedCard : 'simple';
_carpet = ws is WalletLoaded ? ws.wallet.selectedCarpet : 'classic';
_game = HokmGame(
context.read<GameBloc>(),
skin: skin.isEmpty ? 'simple' : skin,
carpet: _carpet.isEmpty ? 'classic' : _carpet,
);
}
@@ -114,6 +117,7 @@ class _GameScreenState extends State<GameScreen> {
}
return Stack(
children: [
// فرش به‌عنوانِ سطحِ میز داخلِ صحنه‌ی Flame (Felt) کشیده می‌شود.
GameWidget(game: _game),
if (!_showSearch(state) && state.state != null)
TableHud(
@@ -65,25 +65,11 @@ class _PrivateEntryScreenState extends State<PrivateEntryScreen> {
final canCreate = unlimited || remaining > 0;
return Column(
children: [
Align(
const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(10),
child: GestureDetector(
onTap: () => context.pop(),
child: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: AppColors.panel,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: AppColors.gold, width: 1.5),
),
child: const Icon(Icons.arrow_back,
color: AppColors.gold),
),
),
padding: EdgeInsets.all(10),
child: AppBackButton(),
),
),
Expanded(
@@ -20,18 +20,22 @@ class PrivateTableScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocConsumer<GameBloc, GameUiState>(
listenWhen: (a, b) =>
(a.notice != b.notice && b.notice != null) ||
(!a.tableClosed && b.tableClosed),
listenWhen:
(a, b) =>
(a.notice != b.notice && b.notice != null) ||
(!a.tableClosed && b.tableClosed),
listener: (context, state) {
if (state.tableClosed) {
if (context.canPop()) context.pop();
return;
}
if (state.notice != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.notice!),
duration: const Duration(seconds: 2)));
duration: const Duration(seconds: 2),
),
);
context.read<GameBloc>().clearNotice();
}
},
@@ -81,11 +85,14 @@ class _LobbyViewState extends State<_LobbyView> {
children: [
_topBar(context, lobby),
Expanded(
child: connecting
? const Center(
child: CircularProgressIndicator(
color: AppColors.gold))
: _content(context, lobby),
child:
connecting
? const Center(
child: CircularProgressIndicator(
color: AppColors.gold,
),
)
: _content(context, lobby),
),
],
),
@@ -102,18 +109,18 @@ class _LobbyViewState extends State<_LobbyView> {
// نوار بالا: بازگشت (چپ) و اشتراک‌گذاریِ کد (راست).
Widget _topBar(BuildContext context, TableLobby? lobby) {
Widget btn(IconData icon, VoidCallback onTap) => GestureDetector(
onTap: onTap,
child: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: AppColors.panel,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.gold, width: 1.5),
),
child: Icon(icon, color: AppColors.gold),
),
);
onTap: onTap,
child: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: AppColors.panel,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.gold, width: 1.5),
),
child: Icon(icon, color: AppColors.gold),
),
);
return Padding(
padding: const EdgeInsets.all(10),
child: Directionality(
@@ -121,13 +128,16 @@ class _LobbyViewState extends State<_LobbyView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
btn(Icons.arrow_back, _leave),
if (lobby != null)
btn(Icons.share, () {
Clipboard.setData(ClipboardData(text: lobby.code));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('کد میز کپی شد؛ برای دوستانت بفرست')));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('کد میز کپی شد؛ برای دوستانت بفرست'),
),
);
}),
btn(Icons.arrow_forward, _leave),
],
),
),
@@ -160,7 +170,9 @@ class _LobbyViewState extends State<_LobbyView> {
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 28, vertical: 36),
horizontal: 28,
vertical: 36,
),
child: Container(
decoration: BoxDecoration(
gradient: const RadialGradient(
@@ -168,29 +180,42 @@ class _LobbyViewState extends State<_LobbyView> {
radius: 0.9,
),
borderRadius: BorderRadius.circular(160),
border: Border.all(color: const Color(0xFF8A5A12), width: 6),
),
child: const Center(
child: GlowText('سلطان حکم', size: 18),
border: Border.all(
color: const Color(0xFF8A5A12),
width: 6,
),
),
child: const Center(child: GlowText('سلطان حکم', size: 18)),
),
),
Align(alignment: Alignment.topCenter, child: _seat(top, false)),
Align(alignment: Alignment.centerLeft, child: _seat(left, false)),
Align(
alignment: Alignment.centerRight, child: _seat(right, false)),
Align(alignment: Alignment.bottomCenter, child: _seat(me, true)),
alignment: Alignment.centerLeft,
child: _seat(left, false),
),
Align(
alignment: Alignment.centerRight,
child: _seat(right, false),
),
Align(
alignment: Alignment.bottomCenter,
child: _seat(me, true),
),
// چرخشِ جایگاه‌ها (فقط میزبان) — تغییرِ تیم‌بندی.
if (lobby.isHost) ...[
Align(
alignment: const Alignment(-1, -0.35),
child: _rotateBtn(Icons.rotate_left,
() => context.read<GameBloc>().rotateTable(-1)),
child: _rotateBtn(
Icons.rotate_left,
() => context.read<GameBloc>().rotateTable(-1),
),
),
Align(
alignment: const Alignment(1, -0.35),
child: _rotateBtn(Icons.rotate_right,
() => context.read<GameBloc>().rotateTable(1)),
child: _rotateBtn(
Icons.rotate_right,
() => context.read<GameBloc>().rotateTable(1),
),
),
],
],
@@ -215,12 +240,16 @@ class _LobbyViewState extends State<_LobbyView> {
onTap: () => context.read<GameBloc>().startTable(_hands),
)
else
const Text('در انتظار شروع توسط میزبان…',
style: TextStyle(color: AppColors.gold, fontSize: 15)),
const Text(
'در انتظار شروع توسط میزبان…',
style: TextStyle(color: AppColors.gold, fontSize: 15),
),
const SizedBox(height: 6),
if (lobby.isHost)
const Text('جای‌های خالی با ربات پر می‌شوند',
style: TextStyle(color: Colors.white54, fontSize: 12)),
const Text(
'جای‌های خالی با ربات پر می‌شوند',
style: TextStyle(color: Colors.white54, fontSize: 12),
),
const SizedBox(height: 20),
],
),
@@ -244,15 +273,20 @@ class _LobbyViewState extends State<_LobbyView> {
color: AppColors.bgDark,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isYou ? AppColors.gold : AppColors.goldDark,
width: 2),
color: isYou ? AppColors.gold : AppColors.goldDark,
width: 2,
),
),
child: empty
? const Icon(Icons.person_add_alt_1, color: Colors.white24)
: ClipRRect(
borderRadius: BorderRadius.circular(9),
child: RandomAvatar(p.name),
),
child:
empty
? const Icon(
Icons.person_add_alt_1,
color: Colors.white24,
)
: ClipRRect(
borderRadius: BorderRadius.circular(9),
child: RandomAvatar(p.name),
),
),
if (!empty)
const Positioned(
@@ -274,8 +308,10 @@ class _LobbyViewState extends State<_LobbyView> {
),
),
if (p?.host == true)
const Text('میزبان',
style: TextStyle(color: AppColors.gold, fontSize: 10)),
const Text(
'میزبان',
style: TextStyle(color: AppColors.gold, fontSize: 10),
),
],
);
}
@@ -314,14 +350,17 @@ class _LobbyViewState extends State<_LobbyView> {
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: selected
? const LinearGradient(
colors: [AppColors.gold, AppColors.goldDark])
: null,
gradient:
selected
? const LinearGradient(
colors: [AppColors.gold, AppColors.goldDark],
)
: null,
color: selected ? null : AppColors.panel,
border: Border.all(
color: selected ? AppColors.gold : AppColors.goldDark,
width: 2),
color: selected ? AppColors.gold : AppColors.goldDark,
width: 2,
),
),
child: Text(
fa[h]!,
@@ -377,8 +416,9 @@ class _CountdownOverlayState extends State<_CountdownOverlay> {
tween: Tween(begin: 0.4, end: 1.2),
duration: const Duration(milliseconds: 700),
curve: Curves.easeOut,
builder: (context, scale, child) =>
Transform.scale(scale: scale, child: child),
builder:
(context, scale, child) =>
Transform.scale(scale: scale, child: child),
child: GlowText(_n > 0 ? '$_n' : 'شروع!', size: 96),
),
);
@@ -20,7 +20,7 @@ class TierListScreen extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(12),
child: Row(children: [
_BackButton(onTap: () => context.pop()),
const AppBackButton(),
const Spacer(),
const GlowText('انتخاب میز', size: 26),
const Spacer(),
@@ -64,25 +64,6 @@ class TierListScreen extends StatelessWidget {
}
}
class _BackButton extends StatelessWidget {
final VoidCallback onTap;
const _BackButton({required this.onTap});
@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.panel,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.gold, width: 1.5),
),
child: const Icon(Icons.arrow_back, color: AppColors.gold),
),
);
}
class _TierCard extends StatelessWidget {
final TableTier tier;
final int index;
@@ -69,14 +69,18 @@ class HokmGame extends FlameGame {
double _hbAccum = 0; // انباشتِ زمان برای تکرارِ منظمِ ضربان تا پایانِ نوبت
bool _wasMyTurn = false;
HokmGame(this.cubit, {this.skin = 'simple'});
/// فرشِ انتخابی؛ 'classic' یعنی سطحِ میزِ ترسیمی. فرشِ تصویری به‌عنوانِ سطحِ
/// میز (بریده در شکلِ میز) روی زمینِ لاجوردی کشیده می‌شود.
final String carpet;
HokmGame(this.cubit, {this.skin = 'simple', this.carpet = 'classic'});
@override
Color backgroundColor() => AppColors.lapisNight;
@override
Future<void> onLoad() async {
add(Felt());
add(Felt(carpet: carpet));
_sub = cubit.stream.listen((ui) {
if (ui.state == null) return;
final s = ui.state!;
@@ -3,22 +3,39 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../../../../../core/network/card_images.dart';
import '../../../../../core/theme/app_theme.dart';
// رنگ‌های مشترکِ میز (منبعِ واحد در AppColors).
const _gold = AppColors.gold;
const _goldDark = AppColors.goldDark;
/// میز: زمینِ سبزِ نمدی در کلِ صفحه + یک «میزِ» مرکزیِ سرمه‌ای با قابِ طلاییِ
/// دولایه (مطابقِ تصویرِ مرجع). کارت‌های زمین داخلِ ناحیه‌ی سرمه‌ای انداخته می‌شوند
/// و پشت‌کارتِ حریفان روی نمدِ سبزِ دورِ آن می‌نشیند.
/// میز: زمینِ لاجوردیِ نمدی در کلِ صفحه + یک «میزِ» مرکزی با قابِ طلاییِ دولایه.
/// اگر فرشی انتخاب شده باشد، سطحِ میز تصویرِ فرش است (بریده‌شده در شکلِ میز)؛
/// وگرنه سطحِ سرمه‌ایِ ترسیمی. کارت‌ها داخلِ همین ناحیه انداخته می‌شوند.
class Felt extends PositionComponent with HasGameReference {
/// شناسه‌ی فرشِ انتخابی؛ 'classic' یعنی سطحِ ترسیمی (بدون تصویر).
final String carpet;
Felt({this.carpet = 'classic'});
Sprite? _carpetSprite;
bool get _hasCarpet => carpet != 'classic';
@override
Future<void> onLoad() async {
if (_hasCarpet) {
_carpetSprite = await CardImages.carpet(carpet);
}
}
@override
void render(Canvas canvas) {
final w = game.size.x, h = game.size.y;
final center = Offset(w / 2, h / 2);
// ۱) زمینِ لاجوردیِ نمدی (کلِ صفحه) با وینیتِ شعاعی برای عمق.
// ۱) زمینِ لاجوردیِ نمدی (کلِ صفحه) با وینیتِ شعاعی برای عمق. همیشه کشیده
// می‌شود تا زیرِ میز/فرش یکدست باشد و لبه‌های فرش با پس‌زمینه جور شود.
canvas.drawRect(
Rect.fromLTWH(0, 0, w, h),
Paint()
@@ -30,7 +47,7 @@ class Felt extends PositionComponent with HasGameReference {
).createShader(Rect.fromLTWH(0, 0, w, h)),
);
// ۲) میزِ مرکزیِ سرمه‌ای با قابِ طلایی.
// ۲) میزِ مرکزی با قابِ طلایی.
final rect = Rect.fromCenter(
center: center, width: w * 0.72, height: h * 0.46);
final radius = Radius.circular(h * 0.05);
@@ -55,17 +72,32 @@ class Felt extends PositionComponent with HasGameReference {
).createShader(rect),
);
// سطحِ سرمه‌ایِ میز با گرادیانِ شعاعی (مرکز روشن‌تر).
canvas.drawRRect(
table,
Paint()
..shader = RadialGradient(
center: Alignment.center,
radius: 0.9,
colors: const [AppColors.lapisLo, AppColors.lapisInk],
stops: const [0.4, 1.0],
).createShader(rect),
);
// سطحِ میز: تصویرِ فرش (بریده در شکلِ میز) یا سطحِ سرمه‌ایِ ترسیمی.
final sprite = _carpetSprite;
if (_hasCarpet && sprite != null) {
canvas.save();
canvas.clipRRect(table);
// فرش را به‌صورتِ cover داخلِ مستطیلِ میز می‌کشیم.
sprite.render(
canvas,
position: Vector2(rect.left, rect.top),
size: Vector2(rect.width, rect.height),
);
// تیره‌ترِ ملایم برای خوانایی کارت‌ها روی فرش.
canvas.drawRRect(table, Paint()..color = const Color(0x1A000000));
canvas.restore();
} else {
canvas.drawRRect(
table,
Paint()
..shader = RadialGradient(
center: Alignment.center,
radius: 0.9,
colors: const [AppColors.lapisLo, AppColors.lapisInk],
stops: const [0.4, 1.0],
).createShader(rect),
);
}
// خطِ طلاییِ نازکِ داخلی (قابِ دولایه).
canvas.drawRRect(