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 { Card(int intRank, int intSuit) : rank = Rank.fromInt(intRank), suit = Suit.fromInt(intSuit), super( size: KlondikeGame.cardSize, ); final Rank rank; final Suit suit; Pile? pile; bool _faceUp = false; bool _isAnimatedFlip = false; bool _isFaceUpView = false; bool _isDragging = false; Vector2 _whereCardStarted = Vector2(0, 0); final List 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 (_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); } 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); break; case 2: _drawSprite(canvas, suitSprite, 0.5, 0.25); _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true); break; 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); break; 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); break; 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); break; 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); break; 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); break; 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); break; 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); break; 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); break; case 11: _drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5); break; case 12: _drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5); break; case 13: _drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5); break; } } 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 onDragStart(DragStartEvent event) { super.onDragStart(event); if (pile?.canMoveCard(this, MoveMethod.drag) ?? false) { _isDragging = true; priority = 100; // Copy each co-ord, else _whereCardStarted changes as the position does. _whereCardStarted = Vector2(position.x, position.y); if (pile is TableauPile) { attachedCards.clear(); 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; // Find out what is under the center-point of this card when it is dropped. final dropPiles = parent! .componentsAtPoint(position + size / 2) .whereType() .toList(); if (dropPiles.isNotEmpty) { if (dropPiles.first.canAcceptCard(this)) { // Found a Pile. // Move card(s) gracefully into position on the receiving pile. 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) { 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.onTapUp(event); } } //#endRegion //#region Effects void doMove( Vector2 to, { double speed = 10.0, double start = 0.0, Curve curve = Curves.easeOutQuad, VoidCallback? onComplete, bool bumpPriority = true, }) { 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'); if (bumpPriority) { priority = 100; } add( MoveToEffect( to, EffectController(duration: dt, startDelay: start, curve: curve), 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 }