Files
Lukas Klingsbo b283b82f6c refactor: Modernize switch; use switch-expressions and no break; (#3133)
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
2024-04-18 23:41:08 +02:00

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;
}
}