fix: some buggs
This commit is contained in:
@@ -37,6 +37,7 @@ class GameState {
|
||||
final List<int> tricksWon;
|
||||
final List<int> scores;
|
||||
final int targetScore;
|
||||
final int turnTimeoutMs; // مهلتِ نوبتِ انسان (برای شمارندهی نوبت)
|
||||
final List<GamePlayer> players;
|
||||
|
||||
GameState({
|
||||
@@ -54,6 +55,7 @@ class GameState {
|
||||
required this.tricksWon,
|
||||
required this.scores,
|
||||
required this.targetScore,
|
||||
required this.turnTimeoutMs,
|
||||
required this.players,
|
||||
});
|
||||
|
||||
@@ -78,6 +80,7 @@ class GameState {
|
||||
tricksWon: ints(j['tricks_won']),
|
||||
scores: ints(j['scores']),
|
||||
targetScore: (j['target_score'] ?? 7) as int,
|
||||
turnTimeoutMs: (j['turn_timeout_ms'] ?? 0) as int,
|
||||
players: ((j['players'] as List?) ?? [])
|
||||
.map((e) => GamePlayer.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
|
||||
@@ -68,13 +68,18 @@ class _GameScreenState extends State<GameScreen> {
|
||||
duration: const Duration(seconds: 2)),
|
||||
);
|
||||
context.read<GameBloc>().clearNotice();
|
||||
// خطا پیش از شروعِ بازی (مثلاً سکهی ناکافی) ⇒ کاربر در دیالوگِ
|
||||
// «جستجوی حریف» گیر نکند؛ به لابی برگردد.
|
||||
if (state.state == null && state.gameOver == null) {
|
||||
_exitToLobby(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.state != null && _introTimer == null) {
|
||||
// کمی «حریفان پیدا شد» نشان بده، بعد اوورلی را کنار بزن و همان
|
||||
// لحظه انیمیشنِ بُر زدن (با صدا) را اجرا کن.
|
||||
_introTimer = Timer(const Duration(milliseconds: 700), () {
|
||||
_introTimer = Timer(const Duration(milliseconds: 1000), () {
|
||||
if (!mounted) return;
|
||||
setState(() => _introHidden = true);
|
||||
_game.endIntro();
|
||||
@@ -90,6 +95,7 @@ class _GameScreenState extends State<GameScreen> {
|
||||
_trumpPicker(context),
|
||||
if (state.handResult != null && state.gameOver == null)
|
||||
_handResult(state),
|
||||
if (!_showSearch(state) && _isMyPlayTurn(state)) _turnBanner(),
|
||||
if (state.gameOver != null) _gameOver(context, state),
|
||||
],
|
||||
);
|
||||
@@ -99,6 +105,34 @@ class _GameScreenState extends State<GameScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
bool _isMyPlayTurn(GameUiState s) =>
|
||||
s.state != null &&
|
||||
s.gameOver == null &&
|
||||
s.state!.phase == 'playing' &&
|
||||
s.state!.isMyTurn &&
|
||||
!s.state!.trickDone;
|
||||
|
||||
// بنرِ «نوبت شماست» وقتی کاربر باید کارت بیندازد.
|
||||
Widget _turnBanner() => Align(
|
||||
alignment: const Alignment(0, 0.46),
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFE9B949), Color(0xFFB8860B)]),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 8)],
|
||||
),
|
||||
child: const Text('نوبت شماست',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF3A0A12),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
bool _showTrumpPicker(GameUiState s) =>
|
||||
s.state != null &&
|
||||
s.state!.phase == 'choose_trump' &&
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _gold = Color(0xFFE9B949);
|
||||
const _goldDark = Color(0xFF8A5A00);
|
||||
|
||||
/// ستارهی طلایی روی بازیکنِ نوبتدار (مطابقِ تصویرِ مرجع).
|
||||
class StarBadge extends PositionComponent {
|
||||
final double r;
|
||||
StarBadge(this.r, Vector2 pos)
|
||||
: super(position: pos, size: Vector2.all(r * 2), anchor: Anchor.center);
|
||||
|
||||
static final _fill = Paint()..color = _gold;
|
||||
static final _stroke = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5
|
||||
..color = _goldDark;
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
final path = _starPath(Offset(r, r), r, r * 0.45, 5);
|
||||
canvas.drawPath(path, _fill);
|
||||
canvas.drawPath(path, _stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// تاجِ طلایی روی حاکم.
|
||||
class CrownBadge extends PositionComponent {
|
||||
final double w;
|
||||
CrownBadge(this.w, Vector2 pos)
|
||||
: super(
|
||||
position: pos,
|
||||
size: Vector2(w, w * 0.8),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
|
||||
static final _fill = Paint()..color = _gold;
|
||||
static final _stroke = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5
|
||||
..color = _goldDark;
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
final h = size.y;
|
||||
final p = Path()
|
||||
..moveTo(0, h) // پایینچپ
|
||||
..lineTo(0, h * 0.35) // نوکِ چپ
|
||||
..lineTo(w * 0.25, h * 0.62) // فرورفتگی
|
||||
..lineTo(w * 0.5, h * 0.18) // نوکِ میانی (بلندترین)
|
||||
..lineTo(w * 0.75, h * 0.62)
|
||||
..lineTo(w, h * 0.35) // نوکِ راست
|
||||
..lineTo(w, h) // پایینراست
|
||||
..close();
|
||||
canvas.drawPath(p, _fill);
|
||||
canvas.drawPath(p, _stroke);
|
||||
// نگینهای نوکِ تاج
|
||||
final gem = Paint()..color = const Color(0xFFB71C1C);
|
||||
canvas.drawCircle(Offset(w * 0.5, h * 0.14), w * 0.05, gem);
|
||||
}
|
||||
}
|
||||
|
||||
Path _starPath(Offset c, double outer, double inner, int points) {
|
||||
final path = Path();
|
||||
for (var i = 0; i < points * 2; i++) {
|
||||
final rad = i.isEven ? outer : inner;
|
||||
final ang = -math.pi / 2 + i * math.pi / points;
|
||||
final pt = Offset(c.dx + rad * math.cos(ang), c.dy + rad * math.sin(ang));
|
||||
i == 0 ? path.moveTo(pt.dx, pt.dy) : path.lineTo(pt.dx, pt.dy);
|
||||
}
|
||||
return path..close();
|
||||
}
|
||||
|
||||
/// افکتِ ضربان (برای جلبِ توجه؛ مثلاً ستارهی نوبت).
|
||||
PulseEffect pulse() => PulseEffect();
|
||||
|
||||
class PulseEffect extends ScaleEffect {
|
||||
PulseEffect()
|
||||
: super.by(
|
||||
Vector2.all(0.25),
|
||||
EffectController(
|
||||
duration: 0.5,
|
||||
reverseDuration: 0.5,
|
||||
infinite: true,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import 'package:flutter/material.dart';
|
||||
import '../../../../../core/service/app_sounds.dart';
|
||||
import '../../../domain/entities/game_entities.dart';
|
||||
import '../../bloc/game_bloc.dart';
|
||||
import 'badges.dart';
|
||||
import 'card_codes.dart';
|
||||
import 'card_component.dart';
|
||||
import 'shuffle_animation.dart';
|
||||
import 'table_pieces.dart';
|
||||
import 'turn_timer.dart';
|
||||
|
||||
/// صحنهی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید.
|
||||
/// کامپوننتهای دست و trick ماندگارند و با افکت حرکت جابهجا میشوند.
|
||||
@@ -46,6 +48,8 @@ class HokmGame extends FlameGame {
|
||||
bool _silent = false; // هنگام خروج از میز، دیگر صدایی پخش نشود
|
||||
bool _introActive = true; // تا پایانِ نمایشِ «جستجوی حریف»
|
||||
bool _pendingShuffle = false; // بُرِ دستِ اول که تا پایانِ اینترو به تعویق افتاده
|
||||
bool _shuffling = false; // در حالِ پخشِ انیمیشنِ بُر زدن (بازیِ کارت ممنوع)
|
||||
String? _prevTrump; // برای تشخیصِ لحظهی انتخابِ حکم
|
||||
|
||||
HokmGame(this.cubit);
|
||||
|
||||
@@ -69,6 +73,12 @@ class HokmGame extends FlameGame {
|
||||
_shuffleNow();
|
||||
}
|
||||
}
|
||||
// لحظهی انتخابِ حکم: صدا + نمایشِ بزرگِ حکم در مرکز.
|
||||
if (_prevTrump == null && s.trump != null && !_silent) {
|
||||
AppSounds.selectHokm();
|
||||
_spawnTrumpReveal(s.trump!);
|
||||
}
|
||||
_prevTrump = s.trump;
|
||||
_prevHandSize = s.yourHand.length;
|
||||
_s = s;
|
||||
_relayout();
|
||||
@@ -76,6 +86,7 @@ class HokmGame extends FlameGame {
|
||||
if (cubit.state.state != null) {
|
||||
_prevTrick = cubit.state.state!.trick.map((t) => t.card).toList();
|
||||
_prevHandSize = cubit.state.state!.yourHand.length;
|
||||
_prevTrump = cubit.state.state!.trump;
|
||||
_s = cubit.state.state;
|
||||
_relayout();
|
||||
}
|
||||
@@ -138,10 +149,41 @@ class HokmGame extends FlameGame {
|
||||
}
|
||||
|
||||
/// اجرای همزمانِ انیمیشن و صدای بُر زدن (صدا با پایانِ انیمیشن قطع میشود).
|
||||
/// در طولِ انیمیشن، بازیِ کارت قفل است تا کارتی وسطِ بُر زدن انداخته نشود.
|
||||
void _shuffleNow() {
|
||||
if (_silent) return;
|
||||
AppSounds.shuffle(cutoffMs: ShuffleAnimation.totalMs);
|
||||
add(ShuffleAnimation(size));
|
||||
_shuffling = true;
|
||||
Future.delayed(const Duration(milliseconds: ShuffleAnimation.totalMs), () {
|
||||
_shuffling = false;
|
||||
if (_s != null && !_silent) _relayout(); // فعالسازیِ دوبارهی کارتها
|
||||
});
|
||||
}
|
||||
|
||||
/// نمایشِ بزرگ و گذرای حکمِ انتخابشده در مرکزِ میز (با پاپ).
|
||||
void _spawnTrumpReveal(String trump) {
|
||||
final (sym, col) = _trumpGlyph(trump);
|
||||
final reveal = TextComponent(
|
||||
text: 'حکم $sym',
|
||||
anchor: Anchor.center,
|
||||
position: size / 2 - Vector2(0, size.y * 0.10),
|
||||
priority: 7000,
|
||||
scale: Vector2.all(0.4),
|
||||
textRenderer: TextPaint(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
color: col,
|
||||
fontSize: size.x * 0.11,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: const [Shadow(color: Colors.black, blurRadius: 6)],
|
||||
),
|
||||
),
|
||||
);
|
||||
reveal.add(ScaleEffect.to(
|
||||
Vector2.all(1.0), EffectController(duration: 0.35, curve: Curves.easeOutBack)));
|
||||
reveal.add(RemoveEffect(delay: 1.3));
|
||||
add(reveal);
|
||||
}
|
||||
|
||||
static bool _isPrefix(List<String> a, List<String> b) {
|
||||
@@ -191,6 +233,8 @@ class HokmGame extends FlameGame {
|
||||
// آیا این کارت در نوبتِ فعلی قابل بازی است (با رعایت follow-suit)؟
|
||||
bool _legal(String code) {
|
||||
final s = _s!;
|
||||
// هنگامِ نمایشِ اینترو یا انیمیشنِ بُر زدن، بازیِ کارت مجاز نیست.
|
||||
if (_introActive || _shuffling) return false;
|
||||
if (s.phase != 'playing' || s.trickDone || !s.isMyTurn) return false;
|
||||
final lead = s.leadSuit;
|
||||
if (lead == null || lead.isEmpty) return true;
|
||||
@@ -441,15 +485,33 @@ class HokmGame extends FlameGame {
|
||||
color: isTurn ? const Color(0xFFE9B949) : Colors.white70,
|
||||
bold: isTurn,
|
||||
);
|
||||
// تاجِ حاکم (روی همهی فازها نشان داده میشود تا حاکم مشخص باشد).
|
||||
if (s.hakem == p.seat) {
|
||||
final crown = CrownBadge(size.x * 0.06, pos - Vector2(0, size.y * 0.066))
|
||||
..priority = 23;
|
||||
_info.add(crown);
|
||||
add(crown);
|
||||
}
|
||||
if (isTurn) {
|
||||
final dot = CircleComponent(
|
||||
radius: size.x * 0.012,
|
||||
anchor: Anchor.center,
|
||||
position: pos - Vector2(0, size.y * 0.03),
|
||||
paint: Paint()..color = const Color(0xFFE9B949),
|
||||
)..priority = 20;
|
||||
_info.add(dot);
|
||||
add(dot);
|
||||
final tpos = pos - Vector2(0, size.y * 0.03);
|
||||
final human = !p.bot && p.connected;
|
||||
// حلقهی شمارشِ نوبت برای بازیکنِ انسان.
|
||||
if (human &&
|
||||
s.turnTimeoutMs > 0 &&
|
||||
(s.phase == 'playing' || s.phase == 'choose_trump')) {
|
||||
final timer = TurnTimer(
|
||||
radius: size.x * 0.03,
|
||||
durationSeconds: s.turnTimeoutMs / 1000.0,
|
||||
position: tpos,
|
||||
)..priority = 20;
|
||||
_info.add(timer);
|
||||
add(timer);
|
||||
}
|
||||
// ستارهی نوبت (مطابقِ تصویرِ مرجع) با ضربان.
|
||||
final star = StarBadge(size.x * 0.015, tpos)..priority = 24;
|
||||
star.add(pulse());
|
||||
_info.add(star);
|
||||
add(star);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// حلقهی شمارشِ معکوسِ نوبت: کمانی که در طولِ مهلتِ نوبت خالی میشود.
|
||||
/// رنگ از طلایی به قرمز میرود و در ثانیههای پایانی هشدار میدهد.
|
||||
class TurnTimer extends PositionComponent {
|
||||
final double radius;
|
||||
final double durationSeconds;
|
||||
double _elapsed = 0;
|
||||
|
||||
TurnTimer({
|
||||
required this.radius,
|
||||
required this.durationSeconds,
|
||||
required Vector2 position,
|
||||
}) : super(
|
||||
position: position,
|
||||
size: Vector2.all(radius * 2),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
|
||||
static final _bg = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3
|
||||
..color = Colors.black54;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
_elapsed += dt;
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
final center = Offset(radius, radius);
|
||||
canvas.drawCircle(center, radius, _bg);
|
||||
|
||||
final frac =
|
||||
durationSeconds <= 0 ? 0.0 : (1 - _elapsed / durationSeconds).clamp(0.0, 1.0);
|
||||
if (frac <= 0) return;
|
||||
|
||||
final color = frac > 0.3 ? const Color(0xFFE9B949) : const Color(0xFFE53935);
|
||||
final arc = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4
|
||||
..strokeCap = StrokeCap.round
|
||||
..color = color;
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-math.pi / 2, // شروع از بالا
|
||||
2 * math.pi * frac,
|
||||
false,
|
||||
arc,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user