@@ -8,26 +8,38 @@ import 'package:flutter/material.dart';
import ' ../game_cubit.dart ' ;
import ' ../game_models.dart ' ;
import ' card_codes.dart ' ;
import ' card_component.dart ' ;
import ' table_pieces.dart ' ;
/// صحنهی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید.
/// کامپوننتهای دست و trick ماندگارند و با افکت حرکت جابهجا میشوند.
///
/// این کلاس فقط «وضعیتِ سرور → چیدمانِ صحنه» را مدیریت میکند:
/// - [_layoutHand] / [_layoutTrick] کارتهای دست و زمین (ماندگار، با انیمیشن).
/// - [_rebuildBacksAndInfo] عناصرِ بازساختهشونده (پشتکارت، شمارندهها، برچسبها).
/// - [_checkCut] + [update]/[render] افکتِ «بریدن با حکم» (تکان + رعد).
class HokmGame extends FlameGame {
final GameCubit cubit ;
// وضعیت بازی و اشتراکِ stream.
GameState ? _s ;
StreamSubscription ? _sub ;
final Map < String , CardComponent > _hand = { } ; // کار تهای دستِ شما (با کد)
final Map < String , CardComponent > _trick = { } ; // کارتهای روی زمین (با کد)
final List < Component > _backs = [ ] ; // پشتکارت حریفان
final List < Component > _info = [ ] ; // نام/امتیاز/حکم/نوبت
// کامپونن تهای ماندگار (با کدِ کارت کلید میخورند) و بازساختهشونده.
final Map < String , CardComponent > _hand = { } ;
final Map < String , CardComponent > _trick = { } ;
final List < Component > _backs = [ ] ;
final List < Component > _info = [ ] ;
// ابعادِ کارتهای روی میز (بر اساس عرض صفحه محاسبه میشود).
double _cardW = 60 ;
double _cardH = 87 ;
double _shake = 0 ; // شدت تکانِ صفحه (افکت بریدن با حکم)
double _t = 0 ; // زمان برای نوسان تکان
List < String > _prevTrick = const [ ] ; // کدِ کارتهای دست جاری (برای تشخیص کارت تازهبازیشده)
// افکتِ تکانِ صفحه هنگام بریدن با حکم.
double _shake = 0 ;
double _t = 0 ;
List < String > _prevTrick = const [ ] ;
HokmGame ( this . cubit ) ;
@@ -36,14 +48,12 @@ class HokmGame extends FlameGame {
@ override
Future < void > onLoad ( ) async {
add ( _Felt ( ) ) ;
add ( Felt ( ) ) ;
_sub = cubit . stream . listen ( ( ui ) {
if ( ui . state ! = null ) {
final s = ui . state ! ;
_checkCut ( s ) ;
_s = s ;
_relayout ( ) ;
}
if ( ui . state = = null ) return ;
_checkCut ( ui . state ! ) ;
_s = ui . state ;
_relayout ( ) ;
} ) ;
if ( cubit . state . state ! = null ) {
_prevTrick = cubit . state . state ! . trick . map ( ( t ) = > t . card ) . toList ( ) ;
@@ -52,21 +62,32 @@ class HokmGame extends FlameGame {
}
}
// اگر دقیقاً یک کارت تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد
@ override
void onGameResize ( Vector2 size ) {
super . onGameResize ( size ) ;
if ( isLoaded ) _relayout ( ) ;
}
@ override
void onRemove ( ) {
_sub ? . cancel ( ) ;
super . onRemove ( ) ;
}
// ===== افکتِ بریدن با حکم (تکان + رعد) =====
// اگر دقیقاً یک کارتِ تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد
// (خالِ زمینه آتو نیست ولی کارتِ تازه آتوست)، همان لحظه تکان + رعد.
void _checkCut ( GameState s ) {
final now = s . trick . map ( ( t ) = > t . card ) . toList ( ) ;
final added = now . length = = _prevTrick . length + 1 & &
_isPrefix ( _prevTrick , now ) ;
if ( added ) {
final last = now . last ;
if ( s . trump ! = null & &
s . leadSuit ! = null & &
s . leadSuit ! = s . trump & &
_suitName ( last ) = = s . trump ) {
_shake = 1.0 ;
add ( _Lightning ( ) ) ;
}
final addedOne = now . length = = _prevTrick . length + 1 & & _isPrefix ( _prevTrick , now ) ;
if ( addedOne & &
s . trump ! = null & &
s . leadSuit ! = null & &
s . leadSuit ! = s . trump & &
suitName ( now . last ) = = s . trump ) {
_shake = 1.0 ;
add ( Lightning ( ) ) ;
}
_prevTrick = now ;
}
@@ -87,58 +108,27 @@ class HokmGame extends FlameGame {
@ override
void render ( Canvas canvas ) {
if ( _shake > 0 ) {
final dx = math . sin ( _t * 55 ) * _shake * size . x * 0.018 ;
final dy = math . cos ( _t * 70 ) * _shake * size . y * 0.010 ;
canvas . save ( ) ;
canvas . translate ( dx , dy ) ;
super . render ( canvas ) ;
canvas . restore ( ) ;
} else {
if ( _shake < = 0 ) {
super . render ( canvas ) ;
return ;
}
final dx = math . sin ( _t * 55 ) * _shake * size . x * 0.018 ;
final dy = math . cos ( _t * 70 ) * _shake * size . y * 0.010 ;
canvas . save ( ) ;
canvas . translate ( dx , dy ) ;
super . render ( canvas ) ;
canvas . restore ( ) ;
}
@ override
void onGameResize ( Vector2 size ) {
super . onGameResize ( size ) ;
if ( isLoaded ) _relayout ( ) ;
}
@ override
void onRemove ( ) {
_sub ? . cancel ( ) ;
super . onRemove ( ) ;
}
// ===== چیدمان =====
// جایگاهِ نسبیِ یک seat نسبت به شما: 0=پایین، 1=چپ، 2=بالا، 3=راست.
int _rel ( int seat ) = > ( seat - _s ! . yourSeat + 4 ) % 4 ;
static String _suitName ( String code ) {
switch ( code [ code . length - 1 ] ) {
case ' H ' :
return ' hearts ' ;
case ' D ' :
return ' diamonds ' ;
case ' C ' :
return ' clubs ' ;
default :
return ' spades ' ;
}
}
bool _legal ( String code ) {
final s = _s ! ;
if ( s . phase ! = ' playing ' | | s . trickDone | | ! s . isMyTurn ) return false ;
final lead = s . leadSuit ;
if ( lead = = null | | lead . isEmpty ) return true ;
final hasLead = s . yourHand . any ( ( c ) = > _suitName ( c ) = = lead ) ;
return ! hasLead | | _suitName ( code ) = = lead ;
}
void _relayout ( ) {
final s = _s ;
if ( s = = null ) return ;
_cardW = size . x * 0.19 ; // کارتهای بزرگتر (مطابق تصویر مرجع)
_cardW = size . x * 0.19 ;
_cardH = _cardW * kCardRatio ;
_rebuildBacksAndInfo ( s ) ;
@@ -146,53 +136,33 @@ class HokmGame extends FlameGame {
_layoutHand ( s ) ;
}
// افکت حرکت بدون انباشتهشدن.
// آیا این کارت در نوبتِ فعلی قابل بازی است (با رعایت follow-suit)؟
bool _legal ( String code ) {
final s = _s ! ;
if ( s . phase ! = ' playing ' | | s . trickDone | | ! s . isMyTurn ) return false ;
final lead = s . leadSuit ;
if ( lead = = null | | lead . isEmpty ) return true ;
final hasLead = s . yourHand . any ( ( c ) = > suitName ( c ) = = lead ) ;
return ! hasLead | | suitName ( code ) = = lead ;
}
// حرکتِ نرمِ یک کارت به مقصد، بدون انباشتهشدنِ افکتها.
void _moveTo ( CardComponent c , Vector2 target , { double dur = 0.38 } ) {
c . children . whereType < MoveToEffect > ( ) . toList ( ) . forEach ( ( e ) = > e . removeFromParent ( ) ) ;
if ( ( c . position - target ) . length < 0.5 ) {
c . position = target ;
return ;
}
c . add ( MoveToEffect (
target , EffectController ( duration: dur , curve: Curves . easeOutCubic ) ) ) ;
c . add ( MoveToEffect ( target , EffectController ( duration: dur , curve: Curves . easeOutCubic ) ) ) ;
}
// ترتیب خالها برای چیدنِ دست (سیاه/قرمز متناوب تا تفکیک بصری راحتتر باشد ).
static const _suitOrder = { ' S ' : 0 , ' H ' : 1 , ' C ' : 2 , ' D ' : 3 } ;
static int _rankValue ( String code ) {
switch ( code . substring ( 0 , code . length - 1 ) ) {
case ' A ' :
return 14 ;
case ' K ' :
return 13 ;
case ' Q ' :
return 12 ;
case ' J ' :
return 11 ;
default :
return int . tryParse ( code . substring ( 0 , code . length - 1 ) ) ? ? 0 ;
}
}
static int _cardCompare ( String a , String b ) {
final sa = _suitOrder [ a [ a . length - 1 ] ] ? ? 0 ;
final sb = _suitOrder [ b [ b . length - 1 ] ] ? ? 0 ;
if ( sa ! = sb ) return sa - sb ;
return _rankValue ( a ) - _rankValue ( b ) ;
}
// ناحیهی وسط میز که رهاکردن کارت در آن یعنی بازی (بالاتر از دستِ شما).
Rect _dropZone ( ) = > Rect . fromLTRB (
size . x * 0.15 ,
size . y * 0.22 ,
size . x * 0.85 ,
size . y * 0.66 ,
) ;
// ناحیهی وسط میز که رهاکردنِ کارت در آن یعنی بازی (بالاتر از دستِ شما ).
Rect _dropZone ( ) = >
Rect . fromLTRB ( size . x * 0.15 , size . y * 0.22 , size . x * 0.85 , size . y * 0.66 ) ;
void _layoutHand ( GameState s ) {
// کارتهای دست بر اساس خال و سپس رتبه مرتب میشوند تا کار بازیکن راحتتر باشد.
final codes = List < String > . from ( s . yourHand ) . . sort ( _cardCompare ) ;
// مرتبسازی بر اساس خال و رتبه تا انتخاب برای بازیکن راحتتر باشد.
final codes = List < String > . from ( s . yourHand ) . . sort ( compareCards ) ;
final n = codes . length ;
// کارتهایی که دیگر در دست نیستند و به trick هم نرفتهاند ⇒ حذف.
@@ -204,8 +174,7 @@ class HokmGame extends FlameGame {
}
// کارتهای دستِ خودتان بزرگتر از کارتهای روی میز.
final hw = size . x * 0.225 ;
final hh = hw * kCardRatio ;
final hw = size . x * 0.225 , hh = hw * kCardRatio ;
final handW = size . x * 0.80 ;
final step = n > 1 ? ( handW - hw ) / ( n - 1 ) : 0.0 ;
final startX = size . x / 2 - ( step * ( n - 1 ) ) / 2 ;
@@ -215,23 +184,17 @@ class HokmGame extends FlameGame {
final code = codes [ i ] ;
final target = Vector2 ( startX + i * step , y ) ;
final legal = _legal ( code ) ;
final dim = s . phase = = ' playing ' & & s . isMyTurn & & ! legal ;
var c = _hand [ code ] ;
if ( c = = null ) {
// کارت جدید ⇒ از مرکز میز پخش میشود.
c = CardComponent (
code: code ,
faceUp: true ,
size: Vector2 ( hw , hh ) ,
position: size / 2 ,
) ;
c = CardComponent ( code: code , faceUp: true , size: Vector2 ( hw , hh ) , position: size / 2 ) ;
_hand [ code ] = c ;
add ( c ) ;
} else {
c . size = Vector2 ( hw , hh ) ;
}
c . onPlay = legal ? ( ) = > cubit . playCard ( code ) : null ;
c . dimmed = dim ;
c . dimmed = s . phase = = ' playing ' & & s . isMyTurn & & ! legal ;
c . home = target ;
c . dropZone = _dropZone ( ) ;
c . restPriority = 10 + i ;
@@ -243,18 +206,16 @@ class HokmGame extends FlameGame {
void _layoutTrick ( GameState s ) {
final present = s . trick . map ( ( t ) = > t . card ) . toSet ( ) ;
// کارتهای trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده (نوبت بعدی) برو و حذف شو.
// کارتهای trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده برو و حذف شو.
for ( final code in _trick . keys . toList ( ) ) {
if ( ! present . contains ( code ) ) {
final c = _trick . remove ( code ) ! ;
final winnerOrigin = _seatOrigin ( _rel ( s . turn ) ) ;
c . children . whereType < MoveToEffect > ( ) . toList ( ) . forEach ( ( e ) = > e . removeFromParent ( ) ) ;
c . add ( SequenceEffect ( [
MoveToEffect ( winnerOrigin ,
EffectController ( duration: 0.25 , curve: Curves . easeIn ) ) ,
RemoveEffect ( ) ,
] ) ) ;
}
if ( present . contains ( code ) ) continue ;
final c = _trick . remove ( code ) ! ;
c . children . whereType < MoveToEffect > ( ) . toList ( ) . forEach ( ( e ) = > e . removeFromParent ( ) ) ;
c . add ( SequenceEffect ( [
MoveToEffect ( _seatOrigin ( _rel ( s . turn ) ) ,
EffectController ( duration: 0.25 , curve: Curves . easeIn ) ) ,
RemoveEffect ( ) ,
] ) ) ;
}
for ( final tc in s . trick ) {
@@ -284,6 +245,7 @@ class HokmGame extends FlameGame {
}
}
// مبدأِ نشستنِ هر جایگاه (برای پرتاب/جمعِ کارتها).
Vector2 _seatOrigin ( int rel ) {
final c = size / 2 ;
switch ( rel ) {
@@ -298,6 +260,7 @@ class HokmGame extends FlameGame {
}
}
// جابهجاییِ کارتِ هر جایگاه از مرکز، در ناحیهی trick.
Vector2 _trickOffset ( int rel ) {
final d = _cardW * 0.62 ;
switch ( rel ) {
@@ -312,6 +275,8 @@ class HokmGame extends FlameGame {
}
}
// ===== عناصرِ بازساختهشونده در هر بهروزرسانی =====
void _rebuildBacksAndInfo ( GameState s ) {
for ( final c in _backs ) {
c . removeFromParent ( ) ;
@@ -322,12 +287,10 @@ class HokmGame extends FlameGame {
_backs . clear ( ) ;
_info . clear ( ) ;
// پشتکارت حریفان
for ( var seat = 0 ; seat < 4 ; seat + + ) {
if ( seat = = s . yourSeat ) continue ;
final rel = _rel ( seat ) ;
final count = seat < s . handCounts . length ? s . handCounts [ seat ] : 0 ;
_addBacks ( rel , count ) ;
_addBacks ( _rel ( seat ) , count ) ;
}
_addTricksWon ( s ) ;
_addScorePucks ( s ) ;
@@ -335,9 +298,8 @@ class HokmGame extends FlameGame {
}
// شمارنده ۱: دستهای بردهی این هَند (tricks_won) — دستهکارتِ پشترو + عدد.
// با هر دست یکی اضافه میشود؛ رسیدن به ۷ یعنی پایان هَند (سرور صفر میکند).
// رسیدن به ۷ یعنی پایان هَند (سرور صفر میکند).
void _addTricksWon ( GameState s ) {
const gold = Color ( 0xFFE9B949 ) ;
for ( var team = 0 ; team < 2 ; team + + ) {
final won = team < s . tricksWon . length ? s . tricksWon [ team ] : 0 ;
if ( won < = 0 ) continue ;
@@ -351,53 +313,29 @@ class HokmGame extends FlameGame {
final n = won . clamp ( 1 , 7 ) ;
final start = base - step * ( ( n - 1 ) / 2 ) ;
for ( var i = 0 ; i < n ; i + + ) {
final c = CardComponent (
code: ' ' ,
faceUp: false ,
size: Vector2 ( w , h ) ,
position: start + step * i . toDouble ( ) ,
priority: 2 ,
) ;
_backs . add ( c ) ;
add ( c ) ;
_addBack ( start + step * i . toDouble ( ) , Vector2 ( w , h ) , priority: 2 ) ;
}
final badge = TextComponent (
text: ' $ won ' ,
anchor: Anchor . center ,
position: base - Vector2 ( 0 , h * 0.7 ) ,
priority: 21 ,
textRenderer: TextPaint (
style: TextStyle (
fontFamily: ' Dana ' ,
color: gold ,
fontSize: _cardW * 0.34 ,
fontWeight: FontWeight . bold ,
shadows: const [ Shadow ( color: Colors . black , blurRadius: 4 ) ] ,
) ,
) ,
) ;
_info . add ( badge ) ;
add ( badge ) ;
_addLabel ( ' $ won ' , base - Vector2 ( 0 , h * 0.7 ) , _cardW * 0.34 , bold: true ) ;
}
}
// شمارنده ۲: امتیاز بازی (scores = هندهای برده) — دیسک مرکزی ِ هر تیم، ۰ تا ۷ .
// رسیدن به ۷ یعنی پایان کل بازی .
// شمارنده ۲: امتیاز بازی (scores = هندهای برده) — دیسکِ هر تیم، ۰ تا ۷.
// یکی جلوی شما (پایین)، دیگری جلوی حریف (راست) — نه جلوی یارِ بالا .
void _addScorePucks ( GameState s ) {
final radius = size . x * 0.05 ;
for ( var team = 0 ; team < 2 ; team + + ) {
final score = team < s . scores . length ? s . scores [ team ] : 0 ;
final mine = team = = s . yourSeat % 2 ;
// یکی جلوی شما (پایین)، دیگری جلوی حریفِ سمت راست — نه جلوی یارِ بالا.
final pos = mine
? Vector2 ( size . x * 0.50 , size . y * 0.74 )
: Vector2 ( size . x * 0.84 , size . y * 0.44 ) ;
final puck = _Puck ( score , radius: radius , position: pos ) . . priority = 22 ;
final puck = ScorePuck ( score , radius: radius , position: pos ) . . priority = 22 ;
_info . add ( puck ) ;
add ( puck ) ;
}
}
// پشتکارتهای یک حریف بهصورت بادبزنی.
void _addBacks ( int rel , int count ) {
if ( count < = 0 ) return ;
final n = count . clamp ( 1 , 13 ) ;
@@ -419,58 +357,29 @@ class HokmGame extends FlameGame {
}
final start = base - stepV * ( ( n - 1 ) / 2 ) ;
for ( var i = 0 ; i < n ; i + + ) {
final comp = CardComponent (
code: ' ' ,
faceUp: false ,
size: Vector2 ( w , h ) ,
position: start + stepV * i . toDouble ( ) ,
priority: 1 ,
) ;
_backs . add ( comp ) ;
add ( comp ) ;
_addBack ( start + stepV * i . toDouble ( ) , Vector2 ( w , h ) , priority: 1 ) ;
}
}
void _addInfo ( GameState s ) {
void addText ( String text , Vector2 pos , double fs , Color color ,
{ Anchor anchor = Anchor . center , bool bold = false } ) {
final t = TextComponent (
text: text ,
anchor: anchor ,
position: pos ,
priority: 20 ,
textRenderer: TextPaint (
style: TextStyle (
fontFamily: ' Dana ' ,
color: color ,
fontSize: fs ,
fontWeight: bold ? FontWeight . bold : FontWeight . normal ) ) ,
) ;
_info . add ( t ) ;
add ( t ) ;
}
if ( s . trump ! = null ) {
final ( sym , col ) = _trumpGlyph ( s . trump ! ) ;
addText ( ' حکم: $ sym ' , Vector2 ( size . x * 0.04 , size . y * 0.04 ) , size . x * 0.05 ,
col , anchor: Anchor . topLeft , bold: true ) ;
_addLabel ( ' حکم: $ sym ' , Vector2 ( size . x * 0.04 , size . y * 0.04 ) , size . x * 0.05 ,
color: col , anchor: Anchor . topLeft , bold: true ) ;
}
addText (
' ${ s . scores . isNotEmpty ? s . scores [ 0 ] : 0 } - ${ s . scores . length > 1 ? s . scores [ 1 ] : 0 } ' ,
Vector2 ( size . x / 2 , size . y * 0.04 ) ,
size . x * 0.05 ,
const Color ( 0xFFE9B949 ) ,
final a = s . scores . isNotEmpty ? s . scores [ 0 ] : 0 ;
final b = s . scores . length > 1 ? s . scores [ 1 ] : 0 ;
_addLabel ( ' $ a - $ b ' , Vector2 ( size . x / 2 , size . y * 0.04 ) , size . x * 0.05 ,
anchor: Anchor . topCenter ) ;
for ( final p in s . players ) {
final rel = _rel ( p . seat ) ;
final pos = _seatLabelPos ( rel ) ;
final pos = _seatLabelPos ( _rel ( p . seat ) ) ;
final isTurn = s . turn = = p . seat ;
addText (
_addLabel (
' ${ p . name } ${ p . bot ? ' (ربات) ' : ' ' } ${ p . connected ? ' ' : ' … ' } ' ,
pos ,
size . x * 0.035 ,
isTurn ? const Color ( 0xFFE9B949 ) : Colors . white70 ,
color: isTurn ? const Color ( 0xFFE9B949 ) : Colors . white70 ,
bold: isTurn ,
) ;
if ( isTurn ) {
@@ -486,6 +395,37 @@ class HokmGame extends FlameGame {
}
}
// ===== کمکیهای ساختِ عنصر (ثبت در فهرستِ بازساختهشونده) =====
void _addBack ( Vector2 pos , Vector2 sz , { required int priority } ) {
final c = CardComponent ( code: ' ' , faceUp: false , size: sz , position: pos , priority: priority ) ;
_backs . add ( c ) ;
add ( c ) ;
}
void _addLabel ( String text , Vector2 pos , double fontSize ,
{ Color color = const Color ( 0xFFE9B949 ) ,
Anchor anchor = Anchor . center ,
bool bold = false } ) {
final t = TextComponent (
text: text ,
anchor: anchor ,
position: pos ,
priority: 21 ,
textRenderer: TextPaint (
style: TextStyle (
fontFamily: ' Dana ' ,
color: color ,
fontSize: fontSize ,
fontWeight: bold ? FontWeight . bold : FontWeight . normal ,
shadows: const [ Shadow ( color: Colors . black , blurRadius: 4 ) ] ,
) ,
) ,
) ;
_info . add ( t ) ;
add ( t ) ;
}
Vector2 _seatLabelPos ( int rel ) {
switch ( rel ) {
case 1 :
@@ -512,116 +452,3 @@ class HokmGame extends FlameGame {
}
}
}
/// افکت رعد روی زمین هنگام برد با حکم؛ پس از مدت کوتاهی خودش حذف میشود.
class _Lightning extends PositionComponent with HasGameReference {
double _life = 0.55 ;
static const _max = 0.55 ;
final _rng = math . Random ( ) ;
final List < List < Offset > > _bolts = [ ] ;
@ override
Future < void > onLoad ( ) async {
size = game . size ;
final center = Offset ( size . x / 2 , size . y / 2 ) ;
for ( var i = 0 ; i < 3 ; i + + ) {
// هر رعد یک خط شکسته از یک نقطهی تصادفیِ کناری به سمت مرکز.
final start = Offset ( _rng . nextDouble ( ) * size . x , _rng . nextDouble ( ) * size . y * 0.4 ) ;
final pts = < Offset > [ start ] ;
const segs = 6 ;
for ( var s = 1 ; s < = segs ; s + + ) {
final t = s / segs ;
final base = Offset . lerp ( start , center , t ) ! ;
final jitter = ( 1 - t ) * size . x * 0.06 ;
pts . add ( base + Offset ( ( _rng . nextDouble ( ) - 0.5 ) * jitter , ( _rng . nextDouble ( ) - 0.5 ) * jitter ) ) ;
}
_bolts . add ( pts ) ;
}
}
@ override
void update ( double dt ) {
_life - = dt ;
if ( _life < = 0 ) removeFromParent ( ) ;
}
@ override
void render ( Canvas canvas ) {
final op = ( _life / _max ) . clamp ( 0.0 , 1.0 ) ;
final glow = Paint ( )
. . color = const Color ( 0xFFFFE082 ) . withValues ( alpha: op * 0.5 )
. . style = PaintingStyle . stroke
. . strokeWidth = size . x * 0.02
. . maskFilter = const MaskFilter . blur ( BlurStyle . normal , 6 ) ;
final core = Paint ( )
. . color = const Color ( 0xFFFFFDE7 ) . withValues ( alpha: op )
. . style = PaintingStyle . stroke
. . strokeWidth = size . x * 0.006
. . strokeCap = StrokeCap . round ;
for ( final bolt in _bolts ) {
final path = Path ( ) . . moveTo ( bolt . first . dx , bolt . first . dy ) ;
for ( final p in bolt . skip ( 1 ) ) {
path . lineTo ( p . dx , p . dy ) ;
}
canvas . drawPath ( path , glow ) ;
canvas . drawPath ( path , core ) ;
}
}
}
/// دیسکِ فلزیِ شمارش دستهای بردهی یک تیم (۰ تا ۷).
class _Puck extends PositionComponent {
final int count ;
_Puck ( this . count , { required double radius , super . position } )
: super ( size: Vector2 . all ( radius * 2 ) , anchor: Anchor . center ) ;
@ override
void render ( Canvas canvas ) {
final r = size . x / 2 ;
final c = Offset ( r , r ) ;
canvas . drawCircle ( c , r , Paint ( ) . . color = const Color ( 0xFF14110F ) ) ;
canvas . drawCircle (
c ,
r * 0.92 ,
Paint ( )
. . style = PaintingStyle . stroke
. . strokeWidth = r * 0.22
. . color = const Color ( 0xFFB8860B ) ,
) ;
final tp = TextPainter (
text: TextSpan (
text: ' $ count ' ,
style: TextStyle (
fontFamily: ' Dana ' ,
color: const Color ( 0xFFE9B949 ) ,
fontSize: r * 1.05 ,
fontWeight: FontWeight . bold ,
) ,
) ,
textDirection: TextDirection . ltr ,
) . . layout ( ) ;
tp . paint ( canvas , c - Offset ( tp . width / 2 , tp . height / 2 ) ) ;
}
}
/// نمدِ سبز میز.
class _Felt extends PositionComponent with HasGameReference {
@ override
void render ( Canvas canvas ) {
final w = game . size . x , h = game . size . y ;
final rect = Rect . fromCenter (
center: Offset ( w / 2 , h / 2 ) ,
width: w * 0.86 ,
height: h * 0.62 ,
) ;
final rrect = RRect . fromRectAndRadius ( rect , Radius . circular ( h * 0.3 ) ) ;
canvas . drawRRect ( rrect , Paint ( ) . . color = const Color ( 0xFF1B5E20 ) ) ;
canvas . drawRRect (
rrect ,
Paint ( )
. . style = PaintingStyle . stroke
. . strokeWidth = 6
. . color = const Color ( 0xFFB8860B ) ,
) ;
}
}