mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +08:00
docs: Klondike Step5, re-work drags and taps (#2894)
The objective of this fix is to make a tap on a card more positive-feeling for the player and not to disappear silently if it is interpreted as a drag. It adds a Base Card to make an empty Stock Pile behave as a Card and use the tap and drag logic of the `Card` class. Any attempted drag on a Stock Pile card, including the Base Card, is now changed to a tap in onTapCancel() and the drag is not followed. The Base Card is rendered in outline only and does *not* take part in gameplay. In other Piles a short drag is either treated as a tap or ignored. Only the Waste and Tableau Piles allow such taps. As before in Klondike Step5, they result in the tapped card moving automatically to its Foundation Pile if it is eligible to "go out". As before in Klondike Step4 and Step5, all piles except the Stock Pile allow drags to start on them and they can finish on a Foundation Pile or a Tableau Pile. Closes #2890. When testing, try sliding the finger or mouse slightly while making a tap. --------- Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
This commit is contained in:
@ -17,7 +17,7 @@ import 'tableau_pile.dart';
|
||||
|
||||
class Card extends PositionComponent
|
||||
with DragCallbacks, TapCallbacks, HasWorldReference<KlondikeWorld> {
|
||||
Card(int intRank, int intSuit)
|
||||
Card(int intRank, int intSuit, {this.isBaseCard = false})
|
||||
: rank = Rank.fromInt(intRank),
|
||||
suit = Suit.fromInt(intSuit),
|
||||
super(
|
||||
@ -27,6 +27,14 @@ class Card extends PositionComponent
|
||||
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;
|
||||
@ -55,6 +63,10 @@ class Card extends PositionComponent
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
if (isBaseCard) {
|
||||
_renderBaseCard(canvas);
|
||||
return;
|
||||
}
|
||||
if (_isFaceUpView) {
|
||||
_renderFront(canvas);
|
||||
} else {
|
||||
@ -86,6 +98,10 @@ class Card extends PositionComponent
|
||||
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()
|
||||
@ -241,16 +257,28 @@ class Card extends PositionComponent
|
||||
|
||||
//#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;
|
||||
// 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;
|
||||
@ -277,6 +305,22 @@ class Card extends PositionComponent
|
||||
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)
|
||||
@ -284,8 +328,7 @@ class Card extends PositionComponent
|
||||
.toList();
|
||||
if (dropPiles.isNotEmpty) {
|
||||
if (dropPiles.first.canAcceptCard(this)) {
|
||||
// Found a Pile.
|
||||
// Move card(s) gracefully into position on the receiving pile.
|
||||
// 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.
|
||||
@ -335,6 +378,12 @@ class Card extends PositionComponent
|
||||
|
||||
@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)) {
|
||||
@ -347,7 +396,7 @@ class Card extends PositionComponent
|
||||
);
|
||||
}
|
||||
} else if (pile is StockPile) {
|
||||
world.stock.onTapUp(event);
|
||||
world.stock.handleTapUp(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
|
||||
import '../klondike_game.dart';
|
||||
import '../pile.dart';
|
||||
@ -9,7 +8,7 @@ import 'card.dart';
|
||||
import 'waste_pile.dart';
|
||||
|
||||
class StockPile extends PositionComponent
|
||||
with TapCallbacks, HasGameReference<KlondikeGame>
|
||||
with HasGameReference<KlondikeGame>
|
||||
implements Pile {
|
||||
StockPile({super.position}) : super(size: KlondikeGame.cardSize);
|
||||
|
||||
@ -31,7 +30,8 @@ class StockPile extends PositionComponent
|
||||
throw StateError('cannot remove cards');
|
||||
|
||||
@override
|
||||
void returnCard(Card card) => throw StateError('cannot remove cards');
|
||||
// Card cannot be removed but could have been dragged out of place.
|
||||
void returnCard(Card card) => card.priority = _cards.indexOf(card);
|
||||
|
||||
@override
|
||||
void acquireCard(Card card) {
|
||||
@ -44,10 +44,11 @@ class StockPile extends PositionComponent
|
||||
|
||||
//#endregion
|
||||
|
||||
@override
|
||||
void onTapUp(TapUpEvent event) {
|
||||
void handleTapUp(Card card) {
|
||||
final wastePile = parent!.firstChild<WastePile>()!;
|
||||
if (_cards.isEmpty) {
|
||||
assert(card.isBaseCard, 'Stock Pile is empty, but no Base Card present');
|
||||
card.position = position; // Force Base Card (back) into correct position.
|
||||
wastePile.removeAllCards().reversed.forEach((card) {
|
||||
card.flip();
|
||||
acquireCard(card);
|
||||
|
||||
@ -22,7 +22,10 @@ class KlondikeGame extends FlameGame<KlondikeWorld> {
|
||||
const Radius.circular(cardRadius),
|
||||
);
|
||||
|
||||
// Constant used when creating Random seed.
|
||||
/// Constant used to decide when a short drag is treated as a TapUp event.
|
||||
static const double dragTolerance = cardWidth / 5;
|
||||
|
||||
/// Constant used when creating Random seed.
|
||||
static const int maxInt = 0xFFFFFFFE; // = (2 to the power 32) - 1
|
||||
|
||||
// This KlondikeGame constructor also initiates the first KlondikeWorld.
|
||||
|
||||
@ -51,6 +51,14 @@ class KlondikeWorld extends World with HasGameReference<KlondikeGame> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Add a Base Card to the Stock Pile, above the pile and below other cards.
|
||||
final baseCard = Card(1, 0, isBaseCard: true);
|
||||
baseCard.position = stock.position;
|
||||
baseCard.priority = -1;
|
||||
baseCard.pile = stock;
|
||||
stock.priority = -2;
|
||||
|
||||
for (var rank = 1; rank <= 13; rank++) {
|
||||
for (var suit = 0; suit < 4; suit++) {
|
||||
final card = Card(rank, suit);
|
||||
@ -64,6 +72,7 @@ class KlondikeWorld extends World with HasGameReference<KlondikeGame> {
|
||||
addAll(foundations);
|
||||
addAll(tableauPiles);
|
||||
addAll(cards);
|
||||
add(baseCard);
|
||||
|
||||
playAreaSize =
|
||||
Vector2(7 * cardSpaceWidth + cardGap, 4 * cardSpaceHeight + topGap);
|
||||
|
||||
Reference in New Issue
Block a user