mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 17:06:50 +08:00 
			
		
		
		
	 b283b82f6c
			
		
	
	b283b82f6c
	
	
	
		
			
			Replaces the switch cases that can be replaces with switch expressions and removes `break;` where it isn't needed. https://dart.dev/language/branches#switch-statements
		
			
				
	
	
		
			497 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			497 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:math';
 | |
| import 'dart:ui';
 | |
| 
 | |
| import 'package:flame/components.dart';
 | |
| import 'package:flame/effects.dart';
 | |
| import 'package:flame/events.dart';
 | |
| import 'package:flutter/animation.dart';
 | |
| 
 | |
| import '../klondike_game.dart';
 | |
| import '../klondike_world.dart';
 | |
| import '../pile.dart';
 | |
| import '../rank.dart';
 | |
| import '../suit.dart';
 | |
| import 'foundation_pile.dart';
 | |
| import 'stock_pile.dart';
 | |
| import 'tableau_pile.dart';
 | |
| 
 | |
| class Card extends PositionComponent
 | |
|     with DragCallbacks, TapCallbacks, HasWorldReference<KlondikeWorld> {
 | |
|   Card(int intRank, int intSuit, {this.isBaseCard = false})
 | |
|       : rank = Rank.fromInt(intRank),
 | |
|         suit = Suit.fromInt(intSuit),
 | |
|         super(
 | |
|           size: KlondikeGame.cardSize,
 | |
|         );
 | |
| 
 | |
|   final Rank rank;
 | |
|   final Suit suit;
 | |
|   Pile? pile;
 | |
| 
 | |
|   // A Base Card is rendered in outline only and is NOT playable. It can be
 | |
|   // added to the base of a Pile (e.g. the Stock Pile) to allow it to handle
 | |
|   // taps and short drags (on an empty Pile) with the same behavior and
 | |
|   // tolerances as for regular cards (see KlondikeGame.dragTolerance) and using
 | |
|   // the same event-handling code, but with different handleTapUp() methods.
 | |
|   final bool isBaseCard;
 | |
| 
 | |
|   bool _faceUp = false;
 | |
|   bool _isAnimatedFlip = false;
 | |
|   bool _isFaceUpView = false;
 | |
|   bool _isDragging = false;
 | |
|   Vector2 _whereCardStarted = Vector2(0, 0);
 | |
| 
 | |
|   final List<Card> attachedCards = [];
 | |
| 
 | |
|   bool get isFaceUp => _faceUp;
 | |
|   bool get isFaceDown => !_faceUp;
 | |
|   void flip() {
 | |
|     if (_isAnimatedFlip) {
 | |
|       // Let the animation determine the FaceUp/FaceDown state.
 | |
|       _faceUp = _isFaceUpView;
 | |
|     } else {
 | |
|       // No animation: flip and render the card immediately.
 | |
|       _faceUp = !_faceUp;
 | |
|       _isFaceUpView = _faceUp;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String toString() => rank.label + suit.label; // e.g. "Q♠" or "10♦"
 | |
| 
 | |
|   //#region Rendering
 | |
| 
 | |
|   @override
 | |
|   void render(Canvas canvas) {
 | |
|     if (isBaseCard) {
 | |
|       _renderBaseCard(canvas);
 | |
|       return;
 | |
|     }
 | |
|     if (_isFaceUpView) {
 | |
|       _renderFront(canvas);
 | |
|     } else {
 | |
|       _renderBack(canvas);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static final Paint backBackgroundPaint = Paint()
 | |
|     ..color = const Color(0xff380c02);
 | |
|   static final Paint backBorderPaint1 = Paint()
 | |
|     ..color = const Color(0xffdbaf58)
 | |
|     ..style = PaintingStyle.stroke
 | |
|     ..strokeWidth = 10;
 | |
|   static final Paint backBorderPaint2 = Paint()
 | |
|     ..color = const Color(0x5CEF971B)
 | |
|     ..style = PaintingStyle.stroke
 | |
|     ..strokeWidth = 35;
 | |
|   static final RRect cardRRect = RRect.fromRectAndRadius(
 | |
|     KlondikeGame.cardSize.toRect(),
 | |
|     const Radius.circular(KlondikeGame.cardRadius),
 | |
|   );
 | |
|   static final RRect backRRectInner = cardRRect.deflate(40);
 | |
|   static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);
 | |
| 
 | |
|   void _renderBack(Canvas canvas) {
 | |
|     canvas.drawRRect(cardRRect, backBackgroundPaint);
 | |
|     canvas.drawRRect(cardRRect, backBorderPaint1);
 | |
|     canvas.drawRRect(backRRectInner, backBorderPaint2);
 | |
|     flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);
 | |
|   }
 | |
| 
 | |
|   void _renderBaseCard(Canvas canvas) {
 | |
|     canvas.drawRRect(cardRRect, backBorderPaint1);
 | |
|   }
 | |
| 
 | |
|   static final Paint frontBackgroundPaint = Paint()
 | |
|     ..color = const Color(0xff000000);
 | |
|   static final Paint redBorderPaint = Paint()
 | |
|     ..color = const Color(0xffece8a3)
 | |
|     ..style = PaintingStyle.stroke
 | |
|     ..strokeWidth = 10;
 | |
|   static final Paint blackBorderPaint = Paint()
 | |
|     ..color = const Color(0xff7ab2e8)
 | |
|     ..style = PaintingStyle.stroke
 | |
|     ..strokeWidth = 10;
 | |
|   static final blueFilter = Paint()
 | |
|     ..colorFilter = const ColorFilter.mode(
 | |
|       Color(0x880d8bff),
 | |
|       BlendMode.srcATop,
 | |
|     );
 | |
|   static final Sprite redJack = klondikeSprite(81, 565, 562, 488);
 | |
|   static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);
 | |
|   static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);
 | |
|   static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)
 | |
|     ..paint = blueFilter;
 | |
|   static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)
 | |
|     ..paint = blueFilter;
 | |
|   static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)
 | |
|     ..paint = blueFilter;
 | |
| 
 | |
|   void _renderFront(Canvas canvas) {
 | |
|     canvas.drawRRect(cardRRect, frontBackgroundPaint);
 | |
|     canvas.drawRRect(
 | |
|       cardRRect,
 | |
|       suit.isRed ? redBorderPaint : blackBorderPaint,
 | |
|     );
 | |
| 
 | |
|     final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;
 | |
|     final suitSprite = suit.sprite;
 | |
|     _drawSprite(canvas, rankSprite, 0.1, 0.08);
 | |
|     _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);
 | |
|     _drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);
 | |
|     _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);
 | |
|     switch (rank.value) {
 | |
|       case 1:
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);
 | |
|       case 2:
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);
 | |
|       case 3:
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);
 | |
|       case 4:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
 | |
|       case 5:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.5);
 | |
|       case 6:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.25);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
 | |
|       case 7:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.35);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
 | |
|       case 8:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.35);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.5);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);
 | |
|       case 9:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.3);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.4);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.4);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
 | |
|       case 10:
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.3);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.4);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.4);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
 | |
|         _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
 | |
|       case 11:
 | |
|         _drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5);
 | |
|       case 12:
 | |
|         _drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5);
 | |
|       case 13:
 | |
|         _drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _drawSprite(
 | |
|     Canvas canvas,
 | |
|     Sprite sprite,
 | |
|     double relativeX,
 | |
|     double relativeY, {
 | |
|     double scale = 1,
 | |
|     bool rotate = false,
 | |
|   }) {
 | |
|     if (rotate) {
 | |
|       canvas.save();
 | |
|       canvas.translate(size.x / 2, size.y / 2);
 | |
|       canvas.rotate(pi);
 | |
|       canvas.translate(-size.x / 2, -size.y / 2);
 | |
|     }
 | |
|     sprite.render(
 | |
|       canvas,
 | |
|       position: Vector2(relativeX * size.x, relativeY * size.y),
 | |
|       anchor: Anchor.center,
 | |
|       size: sprite.srcSize.scaled(scale),
 | |
|     );
 | |
|     if (rotate) {
 | |
|       canvas.restore();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   //#endregion
 | |
| 
 | |
|   //#region Card-Dragging
 | |
| 
 | |
|   @override
 | |
|   void onTapCancel(TapCancelEvent event) {
 | |
|     if (pile is StockPile) {
 | |
|       _isDragging = false;
 | |
|       handleTapUp();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onDragStart(DragStartEvent event) {
 | |
|     super.onDragStart(event);
 | |
|     if (pile is StockPile) {
 | |
|       _isDragging = false;
 | |
|       return;
 | |
|     }
 | |
|     // Clone the position, else _whereCardStarted changes as the position does.
 | |
|     _whereCardStarted = position.clone();
 | |
|     attachedCards.clear();
 | |
|     if (pile?.canMoveCard(this, MoveMethod.drag) ?? false) {
 | |
|       _isDragging = true;
 | |
|       priority = 100;
 | |
|       if (pile is TableauPile) {
 | |
|         final extraCards = (pile! as TableauPile).cardsOnTop(this);
 | |
|         for (final card in extraCards) {
 | |
|           card.priority = attachedCards.length + 101;
 | |
|           attachedCards.add(card);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onDragUpdate(DragUpdateEvent event) {
 | |
|     if (!_isDragging) {
 | |
|       return;
 | |
|     }
 | |
|     final delta = event.localDelta;
 | |
|     position.add(delta);
 | |
|     attachedCards.forEach((card) => card.position.add(delta));
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onDragEnd(DragEndEvent event) {
 | |
|     super.onDragEnd(event);
 | |
|     if (!_isDragging) {
 | |
|       return;
 | |
|     }
 | |
|     _isDragging = false;
 | |
| 
 | |
|     // If short drag, return card to Pile and treat it as having been tapped.
 | |
|     final shortDrag =
 | |
|         (position - _whereCardStarted).length < KlondikeGame.dragTolerance;
 | |
|     if (shortDrag && attachedCards.isEmpty) {
 | |
|       doMove(
 | |
|         _whereCardStarted,
 | |
|         onComplete: () {
 | |
|           pile!.returnCard(this);
 | |
|           // Card moves to its Foundation Pile next, if valid, or it stays put.
 | |
|           handleTapUp();
 | |
|         },
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Find out what is under the center-point of this card when it is dropped.
 | |
|     final dropPiles = parent!
 | |
|         .componentsAtPoint(position + size / 2)
 | |
|         .whereType<Pile>()
 | |
|         .toList();
 | |
|     if (dropPiles.isNotEmpty) {
 | |
|       if (dropPiles.first.canAcceptCard(this)) {
 | |
|         // Found a Pile: move card(s) the rest of the way onto it.
 | |
|         pile!.removeCard(this, MoveMethod.drag);
 | |
|         if (dropPiles.first is TableauPile) {
 | |
|           // Get TableauPile to handle positions, priorities and moves of cards.
 | |
|           (dropPiles.first as TableauPile).dropCards(this, attachedCards);
 | |
|           attachedCards.clear();
 | |
|         } else {
 | |
|           // Drop a single card onto a FoundationPile.
 | |
|           final dropPosition = (dropPiles.first as FoundationPile).position;
 | |
|           doMove(
 | |
|             dropPosition,
 | |
|             onComplete: () {
 | |
|               dropPiles.first.acquireCard(this);
 | |
|             },
 | |
|           );
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Invalid drop (middle of nowhere, invalid pile or invalid card for pile).
 | |
|     doMove(
 | |
|       _whereCardStarted,
 | |
|       onComplete: () {
 | |
|         pile!.returnCard(this);
 | |
|       },
 | |
|     );
 | |
|     if (attachedCards.isNotEmpty) {
 | |
|       attachedCards.forEach((card) {
 | |
|         final offset = card.position - position;
 | |
|         card.doMove(
 | |
|           _whereCardStarted + offset,
 | |
|           onComplete: () {
 | |
|             pile!.returnCard(card);
 | |
|           },
 | |
|         );
 | |
|       });
 | |
|       attachedCards.clear();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   //#endregion
 | |
| 
 | |
|   //#region Card-Tapping
 | |
| 
 | |
|   // Tap a face-up card to make it auto-move and go out (if acceptable), but
 | |
|   // if it is face-down and on the Stock Pile, pass the event to that pile.
 | |
| 
 | |
|   @override
 | |
|   void onTapUp(TapUpEvent event) {
 | |
|     handleTapUp();
 | |
|   }
 | |
| 
 | |
|   void handleTapUp() {
 | |
|     // Can be called by onTapUp or after a very short (failed) drag-and-drop.
 | |
|     // We need to be more user-friendly towards taps that include a short drag.
 | |
|     if (pile?.canMoveCard(this, MoveMethod.tap) ?? false) {
 | |
|       final suitIndex = suit.value;
 | |
|       if (world.foundations[suitIndex].canAcceptCard(this)) {
 | |
|         pile!.removeCard(this, MoveMethod.tap);
 | |
|         doMove(
 | |
|           world.foundations[suitIndex].position,
 | |
|           onComplete: () {
 | |
|             world.foundations[suitIndex].acquireCard(this);
 | |
|           },
 | |
|         );
 | |
|       }
 | |
|     } else if (pile is StockPile) {
 | |
|       world.stock.handleTapUp(this);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   //#endRegion
 | |
| 
 | |
|   //#region Effects
 | |
| 
 | |
|   void doMove(
 | |
|     Vector2 to, {
 | |
|     double speed = 10.0,
 | |
|     double start = 0.0,
 | |
|     int startPriority = 100,
 | |
|     Curve curve = Curves.easeOutQuad,
 | |
|     VoidCallback? onComplete,
 | |
|   }) {
 | |
|     assert(speed > 0.0, 'Speed must be > 0 widths per second');
 | |
|     final dt = (to - position).length / (speed * size.x);
 | |
|     assert(dt > 0, 'Distance to move must be > 0');
 | |
|     add(
 | |
|       CardMoveEffect(
 | |
|         to,
 | |
|         EffectController(duration: dt, startDelay: start, curve: curve),
 | |
|         transitPriority: startPriority,
 | |
|         onComplete: () {
 | |
|           onComplete?.call();
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void doMoveAndFlip(
 | |
|     Vector2 to, {
 | |
|     double speed = 10.0,
 | |
|     double start = 0.0,
 | |
|     Curve curve = Curves.easeOutQuad,
 | |
|     VoidCallback? whenDone,
 | |
|   }) {
 | |
|     assert(speed > 0.0, 'Speed must be > 0 widths per second');
 | |
|     final dt = (to - position).length / (speed * size.x);
 | |
|     assert(dt > 0, 'Distance to move must be > 0');
 | |
|     priority = 100;
 | |
|     add(
 | |
|       MoveToEffect(
 | |
|         to,
 | |
|         EffectController(duration: dt, startDelay: start, curve: curve),
 | |
|         onComplete: () {
 | |
|           turnFaceUp(
 | |
|             onComplete: whenDone,
 | |
|           );
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void turnFaceUp({
 | |
|     double time = 0.3,
 | |
|     double start = 0.0,
 | |
|     VoidCallback? onComplete,
 | |
|   }) {
 | |
|     assert(!_isFaceUpView, 'Card must be face-down before turning face-up.');
 | |
|     assert(time > 0.0, 'Time to turn card over must be > 0');
 | |
|     assert(start >= 0.0, 'Start tim must be >= 0');
 | |
|     _isAnimatedFlip = true;
 | |
|     anchor = Anchor.topCenter;
 | |
|     position += Vector2(width / 2, 0);
 | |
|     priority = 100;
 | |
|     add(
 | |
|       ScaleEffect.to(
 | |
|         Vector2(scale.x / 100, scale.y),
 | |
|         EffectController(
 | |
|           startDelay: start,
 | |
|           curve: Curves.easeOutSine,
 | |
|           duration: time / 2,
 | |
|           onMax: () {
 | |
|             _isFaceUpView = true;
 | |
|           },
 | |
|           reverseDuration: time / 2,
 | |
|           onMin: () {
 | |
|             _isAnimatedFlip = false;
 | |
|             _faceUp = true;
 | |
|             anchor = Anchor.topLeft;
 | |
|             position -= Vector2(width / 2, 0);
 | |
|           },
 | |
|         ),
 | |
|         onComplete: () {
 | |
|           onComplete?.call();
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   //#endregion
 | |
| }
 | |
| 
 | |
| class CardMoveEffect extends MoveToEffect {
 | |
|   CardMoveEffect(
 | |
|     super.destination,
 | |
|     super.controller, {
 | |
|     super.onComplete,
 | |
|     this.transitPriority = 100,
 | |
|   });
 | |
| 
 | |
|   final int transitPriority;
 | |
| 
 | |
|   @override
 | |
|   void onStart() {
 | |
|     super.onStart(); // Flame connects MoveToEffect to EffectController.
 | |
|     parent?.priority = transitPriority;
 | |
|   }
 | |
| }
 |