mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* Introduce generic IAnimatedItem interface for scene animations GameScene's shared 10ms animation timer previously only knew about CardItems (cardsToAnimate). Generalize it so any scene item can tick on the shared timer instead of owning its own QTimer: - New IAnimatedItem interface with a single animationEvent() tick. - GameScene tracks animated items in a QHash keyed by QObject and auto-unregisters items when they are destroyed, so an item deleted mid-animation (concede, removePlayer, deleteLater) can never leave a dangling pointer in the set. - GameScene::~GameScene disconnects incoming connections before the animation timer is deleted; all timer stops are null-guarded so destruction ordering no longer matters. - AbstractCardItem implements IAnimatedItem with a no-op tick so the existing tap-animation registration path keeps working; CardItem overrides it with the real rotate animation. Took 5 minutes * Add Enable/Disable all animations buttons to settings The animation settings group gets two push buttons that toggle every per-effect animation checkbox at once. The base branch carries the buttons and the shared slots; per-effect toggles (life counter, battlefield, arrow draw) are added by the feature branches on top. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
360 lines
11 KiB
C++
360 lines
11 KiB
C++
#include "abstract_card_item.h"
|
|
|
|
#include "../../client/settings/cache_settings.h"
|
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
|
#include "../game_scene.h"
|
|
#include "../z_values.h"
|
|
|
|
#include <QCursor>
|
|
#include <QGraphicsScene>
|
|
#include <QGraphicsSceneMouseEvent>
|
|
#include <QPainter>
|
|
#include <algorithm>
|
|
#include <libcockatrice/card/database/card_database.h>
|
|
#include <libcockatrice/card/database/card_database_manager.h>
|
|
#include <libcockatrice/settings/appearance_settings.h>
|
|
#include <libcockatrice/settings/cards_display_settings.h>
|
|
#include <libcockatrice/settings/debug_settings.h>
|
|
|
|
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
|
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
|
bgColor(Qt::transparent), isHovered(false), realZValue(0)
|
|
{
|
|
setCursor(Qt::OpenHandCursor);
|
|
setFlag(ItemIsSelectable);
|
|
setCacheMode(DeviceCoordinateCache);
|
|
|
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
|
[this] { update(); });
|
|
refreshCardInfo();
|
|
|
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
|
[this](bool _roundCardCorners) {
|
|
Q_UNUSED(_roundCardCorners);
|
|
|
|
prepareGeometryChange();
|
|
update();
|
|
});
|
|
}
|
|
|
|
AbstractCardItem::~AbstractCardItem()
|
|
{
|
|
emit deleteCardInfoPopup(cardRef.name);
|
|
}
|
|
|
|
QRectF AbstractCardItem::boundingRect() const
|
|
{
|
|
return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F);
|
|
}
|
|
|
|
QPainterPath AbstractCardItem::shape() const
|
|
{
|
|
QPainterPath shape;
|
|
qreal cardCornerRadius =
|
|
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
|
|
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
|
|
return shape;
|
|
}
|
|
|
|
void AbstractCardItem::pixmapUpdated()
|
|
{
|
|
update();
|
|
emit sigPixmapUpdated();
|
|
}
|
|
|
|
void AbstractCardItem::refreshCardInfo()
|
|
{
|
|
exactCard = CardDatabaseManager::query()->getCard(cardRef);
|
|
|
|
if (!exactCard && !cardRef.name.isEmpty()) {
|
|
CardInfo::UiAttributes attributes = {.tableRow = -1};
|
|
auto info = CardInfo::newInstance(cardRef.name, "", true, {}, {}, {}, {}, attributes);
|
|
exactCard = ExactCard(info);
|
|
}
|
|
if (exactCard) {
|
|
connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
|
|
}
|
|
|
|
cacheBgColor();
|
|
update();
|
|
}
|
|
|
|
/**
|
|
* Convenience method to get the CardInfo of the exactCard
|
|
* @return A const reference to the CardInfo, or an empty CardInfo if card was null
|
|
*/
|
|
const CardInfo &AbstractCardItem::getCardInfo() const
|
|
{
|
|
return exactCard.getInfo();
|
|
}
|
|
|
|
void AbstractCardItem::setRealZValue(qreal _zValue)
|
|
{
|
|
realZValue = _zValue;
|
|
// During hover, zValue is overridden to HOVERED_CARD. Layout operations
|
|
// like reorganizeCards() call setRealZValue() on all cards including the
|
|
// hovered one — skip setZValue() here to avoid clobbering the override.
|
|
if (!isHovered) {
|
|
setZValue(_zValue);
|
|
}
|
|
}
|
|
|
|
QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
|
{
|
|
return QSizeF(painter->combinedTransform().map(QLineF(0, 0, boundingRect().width(), 0)).length(),
|
|
painter->combinedTransform().map(QLineF(0, 0, 0, boundingRect().height())).length());
|
|
}
|
|
|
|
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
|
{
|
|
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
|
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
|
|
|
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
|
|
|
int scale = resetPainterTransform(painter);
|
|
|
|
painter->translate(totalBoundingRect.width() / 2, totalBoundingRect.height() / 2);
|
|
painter->rotate(angle);
|
|
painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2);
|
|
|
|
QFont f;
|
|
f.setPixelSize(fontSize * scale);
|
|
|
|
painter->setFont(f);
|
|
}
|
|
|
|
void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle)
|
|
{
|
|
qreal scaleFactor = translatedSize.width() / boundingRect().width();
|
|
QPixmap translatedPixmap;
|
|
bool paintImage = true;
|
|
|
|
if (facedown || cardRef.name.isEmpty()) {
|
|
// never reveal card color, always paint the card back
|
|
CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize());
|
|
} else {
|
|
// don't even spend time trying to load the picture if our size is too small
|
|
if (translatedSize.width() > 10) {
|
|
CardPictureLoader::getPixmap(translatedPixmap, exactCard, translatedSize.toSize());
|
|
if (translatedPixmap.isNull()) {
|
|
paintImage = false;
|
|
}
|
|
} else {
|
|
paintImage = false;
|
|
}
|
|
}
|
|
|
|
painter->save();
|
|
|
|
if (paintImage) {
|
|
painter->save();
|
|
painter->setClipPath(shape());
|
|
painter->drawPixmap(boundingRect(), translatedPixmap, QRectF({0, 0}, translatedPixmap.size()));
|
|
painter->restore();
|
|
} else {
|
|
painter->setBrush(bgColor);
|
|
painter->drawPath(shape());
|
|
}
|
|
|
|
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
|
|
painter->save();
|
|
transformPainter(painter, translatedSize, angle);
|
|
painter->setPen(Qt::white);
|
|
painter->setBackground(Qt::black);
|
|
painter->setBackgroundMode(Qt::OpaqueMode);
|
|
QString nameStr;
|
|
if (facedown) {
|
|
nameStr = "# " + QString::number(id);
|
|
} else {
|
|
QString prefix = "";
|
|
if (SettingsCache::instance().debug().getShowCardId()) {
|
|
prefix = "#" + QString::number(id) + " ";
|
|
}
|
|
nameStr = prefix + cardRef.name;
|
|
}
|
|
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
|
|
translatedSize.height() - 6 * scaleFactor),
|
|
Qt::AlignTop | Qt::AlignLeft | Qt::TextWrapAnywhere, nameStr);
|
|
painter->restore();
|
|
}
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
|
{
|
|
painter->save();
|
|
|
|
QSizeF translatedSize = getTranslatedSize(painter);
|
|
paintPicture(painter, translatedSize, tapAngle);
|
|
|
|
painter->setRenderHint(QPainter::Antialiasing, false);
|
|
|
|
if (isSelected() || isHovered) {
|
|
QPen pen;
|
|
if (isHovered) {
|
|
pen.setColor(Qt::yellow);
|
|
}
|
|
if (isSelected()) {
|
|
pen.setColor(Qt::red);
|
|
}
|
|
pen.setWidth(0); // Cosmetic pen
|
|
painter->setPen(pen);
|
|
painter->drawPath(shape());
|
|
}
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
void AbstractCardItem::setCardRef(const CardRef &_cardRef)
|
|
{
|
|
if (cardRef == _cardRef) {
|
|
return;
|
|
}
|
|
|
|
emit deleteCardInfoPopup(cardRef.name);
|
|
if (exactCard) {
|
|
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
|
|
}
|
|
cardRef = _cardRef;
|
|
|
|
refreshCardInfo();
|
|
}
|
|
|
|
void AbstractCardItem::setHovered(bool _hovered)
|
|
{
|
|
if (isHovered == _hovered) {
|
|
return;
|
|
}
|
|
|
|
if (_hovered) {
|
|
processHoverEvent();
|
|
} else {
|
|
// Mark the hovered card's current scene footprint dirty so overlapped
|
|
// sibling zones (e.g. StackZone) repaint after the card moves away.
|
|
if (scene()) {
|
|
scene()->update(sceneBoundingRect());
|
|
}
|
|
}
|
|
|
|
isHovered = _hovered;
|
|
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
|
|
setScale(_hovered && SettingsCache::instance().cardsDisplay().getScaleCards() ? 1.1 : 1);
|
|
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
|
|
update();
|
|
}
|
|
|
|
void AbstractCardItem::setColor(const QString &_color)
|
|
{
|
|
color = _color;
|
|
cacheBgColor();
|
|
update();
|
|
}
|
|
|
|
void AbstractCardItem::cacheBgColor()
|
|
{
|
|
QChar colorChar;
|
|
if (color.isEmpty()) {
|
|
colorChar = exactCard.getInfo().getColorChar();
|
|
} else {
|
|
colorChar = color.at(0);
|
|
}
|
|
|
|
switch (colorChar.toLower().toLatin1()) {
|
|
case 'b':
|
|
bgColor = QColor(0, 0, 0);
|
|
break;
|
|
case 'u':
|
|
bgColor = QColor(0, 140, 180);
|
|
break;
|
|
case 'w':
|
|
bgColor = QColor(255, 250, 140);
|
|
break;
|
|
case 'r':
|
|
bgColor = QColor(230, 0, 0);
|
|
break;
|
|
case 'g':
|
|
bgColor = QColor(0, 160, 0);
|
|
break;
|
|
case 'm':
|
|
bgColor = QColor(250, 190, 30);
|
|
break;
|
|
default:
|
|
bgColor = QColor(230, 230, 230);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
|
|
{
|
|
if (tapped == _tapped) {
|
|
return;
|
|
}
|
|
|
|
tapped = _tapped;
|
|
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
|
} else {
|
|
tapAngle = tapped ? 90 : 0;
|
|
setTransform(QTransform()
|
|
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
|
|
.rotate(tapAngle)
|
|
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
|
|
update();
|
|
}
|
|
}
|
|
|
|
bool AbstractCardItem::animationEvent()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void AbstractCardItem::setFaceDown(bool _facedown)
|
|
{
|
|
facedown = _facedown;
|
|
update();
|
|
}
|
|
|
|
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
|
|
emit cardShiftClicked(cardRef.name);
|
|
} else if ((event->modifiers() & Qt::ControlModifier)) {
|
|
setSelected(!isSelected());
|
|
} else if (!isSelected()) {
|
|
scene()->clearSelection();
|
|
setSelected(true);
|
|
}
|
|
if (event->button() == Qt::LeftButton) {
|
|
setCursor(Qt::ClosedHandCursor);
|
|
} else if (event->button() == Qt::MiddleButton) {
|
|
emit showCardInfoPopup(event->screenPos(), cardRef);
|
|
}
|
|
event->accept();
|
|
}
|
|
|
|
void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
if (event->button() == Qt::MiddleButton) {
|
|
emit deleteCardInfoPopup(cardRef.name);
|
|
}
|
|
|
|
// This function ensures the parent function doesn't mess around with our selection.
|
|
event->accept();
|
|
}
|
|
|
|
void AbstractCardItem::processHoverEvent()
|
|
{
|
|
emit hovered(this);
|
|
}
|
|
|
|
QVariant AbstractCardItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
|
|
{
|
|
if (change == ItemSelectedHasChanged) {
|
|
update();
|
|
return value;
|
|
} else {
|
|
return ArrowTarget::itemChange(change, value);
|
|
}
|
|
}
|