-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCard.cpp
387 lines (316 loc) · 14.8 KB
/
Card.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
// Author: Annie Berend (5033782) - Jonathan Verbeek (5058288)
#include "Card.h"
#include "Main.h"
#include <QDebug>
#include <QMouseEvent>
#include <QGraphicsDropShadowEffect>
#include <QDrag>
#include <QMimeData>
#include <QPainter>
#include <QApplication>
#include <QGraphicsRotation>
// Set cardTileSize and cardSizeFactor
QSize CCard::cardTileSize = QSize(170, 255);
float CCard::cardSizeFactor = 0.6;
// Global streaming operator to serialzie a CardDndPayload into a data stream
QDataStream& operator<<(QDataStream& s, const CardDndPayload* payloadPtr)
{
// Just serialize the pointer
qulonglong ptrval(*reinterpret_cast<qulonglong*>(&payloadPtr));
return s << ptrval;
}
// Global streaming operator to deserialize a CardDndPayload from a data stream
QDataStream& operator>>(QDataStream& s, CardDndPayload*& payloadPtr)
{
// Deserialize the pointer value from the stream
qulonglong ptrval;
s >> ptrval;
// Reinterpret the pointer
payloadPtr = *reinterpret_cast<CardDndPayload**>(&ptrval);
return s;
}
CCard::CCard(QWidget *parent, const ECardSymbol symbol, const ECardType type, const int numberValue)
: QLabel(parent)
, cardSymbol(symbol)
, cardType(type)
, cardNumberValue(numberValue)
, isFlipped(true)
, canInteract(true)
{
// Load the pixmap of the cards tileset
QPixmap tileset(":/assets/card_tileset.png");
// Calculate the position of the card on the tileset
int tilesetX = (type == ECardType::Number ? numberValue - 2 : (int)type);
int tilesetY = (int)symbol;
// Extract that region off the tileset
// We're creating this pixmap on the heap using the copy constructor of QPixmap(const QPixmap&) so we have access to it later
cardFrontPixmap = new QPixmap(tileset.copy(tilesetX * cardTileSize.width(), tilesetY * cardTileSize.height(), cardTileSize.width(), cardTileSize.height()));
// Create the pixmap for the back of the card
cardBackPixmap = new QPixmap(":/assets/card_back.png");
// Flipped by default, also applys the pixmap
this->setCardFlipped(true);
// Apply the screen size to this whole widget's geometry
this->setGeometry(0, 0, cardTileSize.width(), cardTileSize.height());
this->setAlignment(Qt::AlignLeft | Qt::AlignTop);
// Hide the background which this card (a QLabel-child) will inherit from it's parent (the MainWindow)
this->setStyleSheet("background-color: rgba(0, 0, 0, 0); background: transparent;");
// Set up the hover anim to just animate the position property
this->hoverAnim = new QPropertyAnimation(this, "pos");
this->hoverAnim->setDuration(100);
// Set up the flip animation. This is a little bit more complex, we're not animating a property,
// but only a variable.
this->flipAnim = new QVariantAnimation(this);
this->flipAnim->setDuration(500);
this->flipAnim->setStartValue(0.f);
this->flipAnim->setEndValue(179.9f); // When using 180°, it's sometimes confused in which direction to rotate
this->flipAnim->setEasingCurve(QEasingCurve::InOutCubic);
// Called when the animation ticks so we can update
QObject::connect(this->flipAnim, &QVariantAnimation::valueChanged, [=](const QVariant& value) {
// We store a "targetFlipped" property in the animation to remember the target to animate to
bool targetFlipped = this->flipAnim->property("targetFlipped").toBool();
// Create a transform to use for the rotation of the card
QTransform t;
QPoint center = currentPixmap.rect().center();
// Translate to the center, then rotate, then translate back
// That sets the rotation origin to the center instead of the top left corner
t.translate(center.x(), center.y());
t.rotate(value.toReal(), Qt::YAxis);
// If we reached half the animation (we know that by checking whether we already reached the flip target),
// mirror the X axis of the card (as otherwise it'd be mirrored)
if (this->getFlipped() == targetFlipped) t.scale(-1, 1);
// Move the origin back to the top left
t.translate(-center.x(), -center.y());
// Create a new pixmap where we can draw the rotated version on
QPixmap rotatedPixmap(currentPixmap.width(), currentPixmap.height());
// Important, otherwise it will display random bytes
rotatedPixmap.fill(QColor(0, 0, 0, 0));
// Create a new painter and assign the transform
QPainter p(&rotatedPixmap);
p.setTransform(t);
// Enable smooth rendering (from https://forum.qt.io/topic/13290/antialiasing-for-qpixmap-scaled-in-a-qgraphicsscene/5)
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
// Now draw the current pixmap on the rotated pixmap (this will take the transform into account!)
p.drawPixmap(currentPixmap.rect(), currentPixmap);
// If we passed half the animation's time and we have to flip over the card
if (this->flipAnim->currentTime() >= this->flipAnim->duration() / 2 && this->getFlipped() != targetFlipped)
{
// Flip over the card. We do that as the animation alone wouldn't change the card's image between front and back
this->setCardFlipped(targetFlipped);
}
// Apply the newly rotated pixmap. Note that we're not overriding "currentPixmap", so we can use it as a reference
// again next time we're rotating.
this->setPixmap(rotatedPixmap);
});
// Called when the flip animation finishes
QObject::connect(this->flipAnim, &QVariantAnimation::finished, [=] {
// Fall back to the original pixmap to clean up any left over animation states
this->setPixmap(currentPixmap);
});
// Set up the cannot move animation
this->cannotMoveAnim = new QPropertyAnimation(this, "pos");
this->cannotMoveAnim->setDuration(200);
this->cannotMoveAnim->setEasingCurve(QEasingCurve::InOutBounce);
}
QSize CCard::getCardScreenSize()
{
return cardTileSize * cardSizeFactor;
}
void CCard::setCardFlipped(bool shouldFlip)
{
// Set the variable
this->isFlipped = shouldFlip;
// Apply the correct pixmap
QPixmap pixmap = isFlipped ? *cardFrontPixmap : *cardBackPixmap;
// Set the card image to the container, scaled by the desired screensize of the card
currentPixmap = pixmap.scaled(getCardScreenSize().width(), getCardScreenSize().height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
this->setPixmap(currentPixmap);
// Create a drop shadow effect to make it easier to distinguish cards on one stack
QGraphicsDropShadowEffect* DropShadowEffect = new QGraphicsDropShadowEffect();
DropShadowEffect->setBlurRadius(50);
DropShadowEffect->setOffset(0, 0);
DropShadowEffect->setColor(QColor(0, 0, 0, 64));
this->setGraphicsEffect(DropShadowEffect);
}
void CCard::requestCardFlip(bool shouldFlip)
{
// Set the targetFlipped property so the animation knows where to stop
this->flipAnim->setProperty("targetFlipped", shouldFlip);
// Start the animation. setCardFlipped will be called from the animation when it's time
this->flipAnim->start();
// Play a sound
CMain::get()->getSoundManager()->playSoundEffect(ESoundEffectType::CardFlip);
// Flipping a card changes the score
CMain::get()->getGameInstance()->addScore(GameScoringAttributes::TURN_OVER_TABLEAU_CARD);
}
void CCard::setCardStack(CCardStack *newStack)
{
currentStack = newStack;
}
QString CCard::toString()
{
QString str;
QTextStream stream(&str);
// Format type and symbol strings
QString typeStr, symbolStr;
switch (getType()) {
case ECardType::Number: typeStr = "Number"; break;
case ECardType::Jack: typeStr = "Jack"; break;
case ECardType::King: typeStr = "King"; break;
case ECardType::Queen: typeStr = "Queen"; break;
case ECardType::Ace: typeStr = "Ace"; break;
}
switch (getSymbol()) {
case ECardSymbol::Heart: symbolStr = "Heart"; break;
case ECardSymbol::Diamond: symbolStr = "Diamond"; break;
case ECardSymbol::Spade: symbolStr = "Spade"; break;
case ECardSymbol::Club: symbolStr = "Club"; break;
}
// Create final string
stream << "(Card " << this << " type=" << typeStr << " symbol=" << symbolStr << " numVal=" << getNumberValue() << " overallValue=" << getOverallValue() << ")";
return str;
}
void CCard::setCanInteract(bool canInteract)
{
this->canInteract = canInteract;
}
void CCard::mousePressEvent(QMouseEvent* ev)
{
// If the left mouse button was pressed, is flipped and we can interact
if (ev->button() == Qt::LeftButton && this->getFlipped() && canInteract)
{
// Remember this position
dragStartPos = ev->pos();
}
}
void CCard::mouseReleaseEvent(QMouseEvent* ev)
{
// Only trigger when didn't move
if ((ev->pos() - dragStartPos).manhattanLength() < 1)
{
// Try to move the card
bool didMoveCard = CMain::get()->getGameInstance()->moveCard(this, getCardStack());
// If moving didn't work and we're not playing the "cannot move" animation already
if (!didMoveCard && this->cannotMoveAnim->state() != QAbstractAnimation::Running)
{
// Set up the keyframes and play the animation
this->cannotMoveAnim->setKeyValueAt(0, this->pos());
this->cannotMoveAnim->setKeyValueAt(0.35, this->pos() - QPoint(10, 0));
this->cannotMoveAnim->setKeyValueAt(0.65, this->pos() + QPoint(10, 0));
this->cannotMoveAnim->setKeyValueAt(1, this->pos());
this->cannotMoveAnim->start();
}
// Play the sound
CMain::get()->getSoundManager()->playSoundEffect(didMoveCard ? ESoundEffectType::CardClick : ESoundEffectType::CardCannotMove);
}
}
void CCard::mouseMoveEvent(QMouseEvent* ev)
{
// Make sure the left button is held and we dragged a minimum distance to further handle the dragging
if (!(ev->buttons() & Qt::LeftButton) || (ev->pos() - dragStartPos).manhattanLength() < 1 || !this->getFlipped() || !canInteract) return;
// Create a new QDrag object to enable drag and drop
QDrag* drag = new QDrag(this);
// Every drag'n'drop action needs MIME data to identify whether drops are allowed at certain
// places or not.
QMimeData* mimeData = new QMimeData();
CardDndPayload* payload = new CardDndPayload();
// Try to cast the stack this card is currently on to a holding stack
CHoldingStack* myHoldingStack = dynamic_cast<CHoldingStack*>(this->getCardStack());
// If this card is in a holding stack
if (myHoldingStack)
{
// Get all cards above this card
QList<CCard*> cardsAboveMe = myHoldingStack->getCardsAbove(this);
// Draw a pixmap containing all cards of the "substack"
QPixmap dragPixmap = QPixmap(this->pixmap()->width(), this->pixmap()->height() + ((cardsAboveMe.length() - 1) * CHoldingStack::CardOffsetInStack));
QPainter painter;
painter.begin(&dragPixmap);
for (int i = 0; i < cardsAboveMe.length(); i++)
{
CCard* c = cardsAboveMe[i];
painter.drawPixmap(0, i * CHoldingStack::CardOffsetInStack, c->pixmap()->width(), c->pixmap()->height(), *c->pixmap());
// Add this card to the payload
payload->cards.push_back(c);
// Hide all the "real" cards
c->hide();
}
// Not a single card
payload->isSingleCard = false;
// Stop painting and set the pixmap
painter.end();
drag->setPixmap(dragPixmap);
}
else
{
// Just draw the pixmap of this card
drag->setPixmap(*this->pixmap());
// Hide this card
this->hide();
// Add this single card to the dnd payload
payload->cards.push_back(this);
payload->isSingleCard = true;
}
// Serialize the payload into the MIME data
QByteArray payloadData;
QDataStream payloadDataStream(&payloadData, QIODevice::WriteOnly);
payloadDataStream << payload;
mimeData->setData("application/x-solitaire-dnd", payloadData);
// Apply the new mime data and set the pixmap image to show when dragging this card
drag->setMimeData(mimeData);
// ev->pos() is relative to the pivot point of the card (topleft)
// this->pos() is relative to the parent widget (the card stack)
// Therefore we need to map these coordinates first to a common coordinate origin, which is
// the main window in this case
QPoint mousePosInWindow = this->mapTo(CMain::get()->getGameWindow(), ev->pos());
QPoint cardPosInWindow = this->mapTo(CMain::get()->getGameWindow(), this->pos());
// The "hot spot" is the offset in pixels used to offset the pixmap from the mouse cursor when
// dragging. Subtracting this->pos() from the cardPosInWindow makes sure that the offset of the
// card in it's parent container (the stacks) is not taken into account. More on that here:
// https://doc.qt.io/archives/qt-4.8/qdrag.html
QPoint dndHotSpot = mousePosInWindow - (cardPosInWindow - this->pos());
drag->setHotSpot(dndHotSpot);
// Paint a gray overlay to indicate this is a card we're dragging
// TODO: a better visual effect?
QPainter painter;
QPixmap temp = drag->pixmap();
painter.begin(&temp);
painter.setCompositionMode(QPainter::CompositionMode_Multiply);
painter.fillRect(temp.rect(), QColor(0, 0, 0, 127));
painter.end();
drag->setPixmap(temp);
// Start the drag and drop action. This call is blocking!
Qt::DropAction dropAction = drag->exec(Qt::MoveAction);
Q_UNUSED(dropAction);
// Since the above call is blocking, code here get's executed once the d'n'd operation
// is finished. Show all cards again
for (CCard* card : payload->cards)
{
card->show();
}
}
void CCard::enterEvent(QEvent* ev)
{
Q_UNUSED(ev);
// Only play the hover animation if this card is flipped and not playing the animation already
if (this->getFlipped() && this->hoverAnim->state() == QAbstractAnimation::Stopped && canInteract)
{
this->hoverAnim->setStartValue(this->pos());
this->hoverAnim->setEndValue(this->pos() + QPoint(0, 10));
this->hoverAnim->setDirection(QPropertyAnimation::Direction::Forward);
stackBeforeHoverAnim = this->getCardStack();
this->hoverAnim->start();
}
}
void CCard::leaveEvent(QEvent* ev)
{
Q_UNUSED(ev);
// Check whether the stack changed (e.g. because the card was moved)
bool didStackChange = stackBeforeHoverAnim != this->getCardStack();
// Don't play this animation if not flipped, or because this card was moved by code and not by hand
if (this->getFlipped() && !didStackChange && canInteract)
{
this->hoverAnim->setDirection(QPropertyAnimation::Direction::Backward);
this->hoverAnim->start();
}
// Reset the wasClicked flag if set
if (didStackChange) stackBeforeHoverAnim = this->getCardStack();
}