mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-03 20:36:31 +08:00
When the cards are dealt at the start of a Klondike Step 5 game there are multiple occurrences of unrealistic behaviors: - Several cards being dealt may come from the middle of the Stock Pile, - Cards that are dealt early can be seen moving in front of cards that are dealt later. Each card dealt should be rendered on the top of the Stock Pile and should be seen to come from there. During the deal, later cards dealt are placed on top of earlier ones, so later cards should be seen to move in front of earlier ones when they are travelling from the Stock Pile to their positions in the layout. Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
461 lines
14 KiB
Dart
461 lines
14 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)
|
|
: 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<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 (_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<Pile>()
|
|
.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,
|
|
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;
|
|
}
|
|
}
|