mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-07 10:33:24 -08:00
Holding or spamming the draw-arrow shortcut fires the action again before the previous local arrow has been placed. Every new arrow takes the mouse grab from the previous one, so the previous arrow never receives the release that deletes it. If the cursor had passed over a card meanwhile, the orphan stayed visible on the board. Track the in-progress arrow on the CardItem that started it and cancel it, together with any child arrows, before starting a new one. The attach-arrow shortcut (Ctrl+Alt+A) uses the same path, so it no longer leaks arrows either. Fixes #4329. Co-authored-by: mb <mbaecker@unforce.de>
552 lines
No EOL
17 KiB
C++
552 lines
No EOL
17 KiB
C++
#include "card_item.h"
|
|
|
|
#include "../../client/settings/cache_settings.h"
|
|
#include "../card_dimensions.h"
|
|
#include "../game_scene.h"
|
|
#include "arrow_item.h"
|
|
#include "card_drag_item.h"
|
|
#include "libcockatrice/card/printing/exact_card.h"
|
|
#include "libcockatrice/protocol/pb/serverinfo_cardcounter.pb.h"
|
|
|
|
#include <../../client/settings/card_counter_settings.h>
|
|
#include <QApplication>
|
|
#include <QBrush>
|
|
#include <QChar>
|
|
#include <QColor>
|
|
#include <QCursor>
|
|
#include <QFlags>
|
|
#include <QGraphicsItem>
|
|
#include <QGraphicsScene>
|
|
#include <QGraphicsSceneMouseEvent>
|
|
#include <QMap>
|
|
#include <QMapIterator>
|
|
#include <QObject>
|
|
#include <QPainter>
|
|
#include <QPen>
|
|
#include <QRectF>
|
|
#include <QSharedPointer>
|
|
#include <QSizeF>
|
|
#include <QTransform>
|
|
#include <QVariant>
|
|
#include <QtGlobal>
|
|
#include <libcockatrice/card/card_info.h>
|
|
#include <libcockatrice/game/abstract_game.h>
|
|
#include <libcockatrice/game/board/card_state.h>
|
|
#include <libcockatrice/game/game_state.h>
|
|
#include <libcockatrice/game/phase.h>
|
|
#include <libcockatrice/game/player/player_actions.h>
|
|
#include <libcockatrice/game/player/player_info.h>
|
|
#include <libcockatrice/game/player/player_logic.h>
|
|
#include <libcockatrice/game/player/player_manager.h>
|
|
#include <libcockatrice/game/zones/card_zone_logic.h>
|
|
#include <libcockatrice/game/zones/table_zone_logic.h>
|
|
#include <libcockatrice/game/zones/view_zone_logic.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
|
#include <libcockatrice/settings/interface_settings.h>
|
|
#include <libcockatrice/utility/card_ref.h>
|
|
#include <qnamespace.h>
|
|
|
|
struct CardRef;
|
|
|
|
CardItem::CardItem(PlayerLogic *_owner,
|
|
QGraphicsItem *parent,
|
|
const CardRef &cardRef,
|
|
int _cardid,
|
|
CardZoneLogic *_zone)
|
|
: AbstractCardItem(parent, cardRef, _owner, _cardid), dragItem(nullptr)
|
|
{
|
|
state->setZone(_zone);
|
|
owner->addCard(this);
|
|
|
|
connect(&SettingsCache::instance().cardCounters(), &CardCounterSettings::colorChanged, this, [this](int counterId) {
|
|
if (state->getCounters().contains(counterId)) {
|
|
update();
|
|
}
|
|
});
|
|
|
|
connect(state, &CardState::stateReset, this, [this] {
|
|
if (scene()) {
|
|
static_cast<GameScene *>(scene())->unregisterAnimationItem(this);
|
|
}
|
|
update();
|
|
});
|
|
|
|
// The retyped logic layer drives CardState setters directly (bypassing the CardItem wrappers that called
|
|
// update()), so repaint on every state change: attacking, annotation, doesntUntap, P/T and counters.
|
|
connect(state, &CardState::stateChanged, this, [this] { update(); });
|
|
|
|
connect(state, &CardState::viewDeleteRequested, this, &CardItem::deleteLater);
|
|
}
|
|
|
|
void CardItem::prepareDelete()
|
|
{
|
|
if (owner != nullptr) {
|
|
auto *gameScene = qobject_cast<GameScene *>(scene());
|
|
if (gameScene && gameScene->getActiveCard() == this) {
|
|
emit owner->requestCardMenuUpdate(nullptr);
|
|
gameScene->setActiveCard(nullptr);
|
|
}
|
|
owner = nullptr;
|
|
}
|
|
|
|
const QList<CardState *> &attachedStates = state->getAttachedCards();
|
|
while (!attachedStates.isEmpty()) {
|
|
auto *attachedItem = qobject_cast<CardItem *>(attachedStates.first()->parent());
|
|
if (attachedItem == nullptr) {
|
|
break;
|
|
}
|
|
attachedItem->setZone(nullptr); // so that it won't try to call reorganizeCards()
|
|
attachedItem->setAttachedTo(nullptr);
|
|
}
|
|
|
|
if (state->getAttachedTo() != nullptr) {
|
|
state->getAttachedTo()->removeAttachedCard(state);
|
|
state->setAttachedTo(nullptr);
|
|
}
|
|
}
|
|
|
|
void CardItem::deleteLater()
|
|
{
|
|
prepareDelete();
|
|
if (scene()) {
|
|
static_cast<GameScene *>(scene())->unregisterAnimationItem(this);
|
|
}
|
|
AbstractCardItem::deleteLater();
|
|
}
|
|
|
|
void CardItem::setZone(CardZoneLogic *_zone)
|
|
{
|
|
state->setZone(_zone);
|
|
}
|
|
|
|
void CardItem::retranslateUi()
|
|
{
|
|
}
|
|
|
|
void CardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
|
{
|
|
auto &cardCounterSettings = SettingsCache::instance().cardCounters();
|
|
|
|
painter->save();
|
|
AbstractCardItem::paint(painter, option, widget);
|
|
|
|
int i = 0;
|
|
QMapIterator<int, int> counterIterator(state->getCounters());
|
|
while (counterIterator.hasNext()) {
|
|
counterIterator.next();
|
|
QColor _color = cardCounterSettings.color(counterIterator.key());
|
|
|
|
paintNumberEllipse(counterIterator.value(), 14, _color, i, state->getCounters().size(), painter);
|
|
++i;
|
|
}
|
|
|
|
QSizeF translatedSize = getTranslatedSize(painter);
|
|
qreal scaleFactor = translatedSize.width() / boundingRect().width();
|
|
|
|
if (!state->getPT().isEmpty()) {
|
|
painter->save();
|
|
transformPainter(painter, translatedSize, tapAngle);
|
|
|
|
if (!getFaceDown() && state->getPT() == state->getCardInfo().getPowTough()) {
|
|
painter->setPen(Qt::white);
|
|
} else {
|
|
painter->setPen(QColor(255, 150, 0)); // dark orange
|
|
}
|
|
|
|
painter->setBackground(Qt::black);
|
|
painter->setBackgroundMode(Qt::OpaqueMode);
|
|
|
|
painter->drawText(QRectF(4 * scaleFactor, 4 * scaleFactor, translatedSize.width() - 10 * scaleFactor,
|
|
translatedSize.height() - 8 * scaleFactor),
|
|
Qt::AlignRight | Qt::AlignBottom, state->getPT());
|
|
painter->restore();
|
|
}
|
|
|
|
if (!state->getAnnotation().isEmpty()) {
|
|
painter->save();
|
|
|
|
transformPainter(painter, translatedSize, tapAngle);
|
|
painter->setBackground(Qt::black);
|
|
painter->setBackgroundMode(Qt::OpaqueMode);
|
|
painter->setPen(Qt::white);
|
|
|
|
painter->drawText(QRectF(4 * scaleFactor, 4 * scaleFactor, translatedSize.width() - 8 * scaleFactor,
|
|
translatedSize.height() - 8 * scaleFactor),
|
|
Qt::AlignCenter | Qt::TextWrapAnywhere, state->getAnnotation());
|
|
painter->restore();
|
|
}
|
|
|
|
if (getBeingPointedAt()) {
|
|
painter->fillPath(shape(), QBrush(QColor(255, 0, 0, 100)));
|
|
}
|
|
|
|
if (state->getDoesntUntap()) {
|
|
painter->save();
|
|
|
|
painter->setRenderHint(QPainter::Antialiasing, false);
|
|
|
|
QPen pen;
|
|
pen.setColor(Qt::magenta);
|
|
pen.setWidth(0); // Cosmetic pen
|
|
painter->setPen(pen);
|
|
painter->drawPath(shape());
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
void CardItem::setAttacking(bool _attacking)
|
|
{
|
|
state->setAttacking(_attacking);
|
|
update();
|
|
}
|
|
|
|
void CardItem::setCounter(int _id, int _value)
|
|
{
|
|
state->setCounter(_id, _value);
|
|
update();
|
|
}
|
|
|
|
void CardItem::setAnnotation(const QString &_annotation)
|
|
{
|
|
state->setAnnotation(_annotation);
|
|
update();
|
|
}
|
|
|
|
void CardItem::setDoesntUntap(bool _doesntUntap)
|
|
{
|
|
state->setDoesntUntap(_doesntUntap);
|
|
update();
|
|
}
|
|
|
|
void CardItem::setPT(const QString &_pt)
|
|
{
|
|
state->setPT(_pt);
|
|
update();
|
|
}
|
|
|
|
void CardItem::setAttachedTo(CardItem *_attachedTo)
|
|
{
|
|
CardState *newParent = _attachedTo == nullptr ? nullptr : _attachedTo->getState();
|
|
state->setAttachedTo(newParent);
|
|
|
|
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its attached
|
|
// cards
|
|
if (newParent != nullptr ? newParent->getZone() == nullptr : state->getZone() == nullptr) {
|
|
deleteLater();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Resets the fields that should be reset after a zone transition
|
|
*/
|
|
void CardItem::resetState(bool keepAnnotations)
|
|
{
|
|
state->resetState(keepAnnotations);
|
|
}
|
|
|
|
void CardItem::processCardInfo(const ServerInfo_Card &_info)
|
|
{
|
|
state->clearCounters();
|
|
const int counterListSize = _info.counter_list_size();
|
|
for (int i = 0; i < counterListSize; ++i) {
|
|
const ServerInfo_CardCounter &counterInfo = _info.counter_list(i);
|
|
state->insertCounter(counterInfo.id(), counterInfo.value());
|
|
}
|
|
|
|
setId(_info.id());
|
|
setCardRef({QString::fromStdString(_info.name()), QString::fromStdString(_info.provider_id())});
|
|
setAttacking(_info.attacking());
|
|
setFaceDown(_info.face_down());
|
|
setPT(QString::fromStdString(_info.pt()));
|
|
setAnnotation(QString::fromStdString(_info.annotation()));
|
|
setColor(QString::fromStdString(_info.color()));
|
|
setTapped(_info.tapped());
|
|
setDestroyOnZoneChange(_info.destroy_on_zone_change());
|
|
setDoesntUntap(_info.doesnt_untap());
|
|
}
|
|
|
|
CardDragItem *CardItem::createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown)
|
|
{
|
|
deleteDragItem();
|
|
dragItem = new CardDragItem(this, _id, _pos, forceFaceDown);
|
|
dragItem->setVisible(false);
|
|
scene()->addItem(dragItem);
|
|
dragItem->updatePosition(_scenePos);
|
|
dragItem->setVisible(true);
|
|
|
|
return dragItem;
|
|
}
|
|
|
|
void CardItem::deleteDragItem()
|
|
{
|
|
if (dragItem) {
|
|
dragItem->deleteLater();
|
|
}
|
|
dragItem = nullptr;
|
|
}
|
|
|
|
void CardItem::drawArrow(const QColor &arrowColor)
|
|
{
|
|
if (owner->getGame()->getPlayerManager()->isSpectator()) {
|
|
return;
|
|
}
|
|
|
|
// A held shortcut repeats the action before the previous arrow is placed.
|
|
// That arrow has lost the mouse grab to the new one, so it would never
|
|
// receive the release that deletes it; discard it (and its children).
|
|
if (pendingArrow) {
|
|
pendingArrow->cancel();
|
|
}
|
|
|
|
auto *game = owner->getGame();
|
|
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
|
int phase = 0; // 0 means to not set the phase
|
|
if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) {
|
|
int currentPhase = game->getGameState()->getCurrentPhase();
|
|
phase = Phases::getLastSubphase(currentPhase) + 1;
|
|
}
|
|
ArrowDragItem *arrow = new ArrowDragItem(arrowOwner, this, arrowColor, phase);
|
|
scene()->addItem(arrow);
|
|
pendingArrow = arrow;
|
|
arrow->grabMouse();
|
|
|
|
for (const auto &item : scene()->selectedItems()) {
|
|
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
|
if (card == nullptr || card == this) {
|
|
continue;
|
|
}
|
|
if (card->getZone() != state->getZone()) {
|
|
continue;
|
|
}
|
|
|
|
ArrowDragItem *childArrow = new ArrowDragItem(arrowOwner, card, arrowColor, phase);
|
|
scene()->addItem(childArrow);
|
|
arrow->addChildArrow(childArrow);
|
|
}
|
|
}
|
|
|
|
void CardItem::drawAttachArrow()
|
|
{
|
|
if (owner->getGame()->getPlayerManager()->isSpectator()) {
|
|
return;
|
|
}
|
|
|
|
// See drawArrow(): a repeated trigger must not orphan the previous arrow.
|
|
if (pendingArrow) {
|
|
pendingArrow->cancel();
|
|
}
|
|
|
|
auto *arrow = new ArrowAttachItem(this);
|
|
scene()->addItem(arrow);
|
|
pendingArrow = arrow;
|
|
arrow->grabMouse();
|
|
|
|
for (const auto &item : scene()->selectedItems()) {
|
|
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
|
if (card == nullptr || card == this) {
|
|
continue;
|
|
}
|
|
if (card->getZone() != state->getZone()) {
|
|
continue;
|
|
}
|
|
|
|
ArrowAttachItem *childArrow = new ArrowAttachItem(card);
|
|
scene()->addItem(childArrow);
|
|
arrow->addChildArrow(childArrow);
|
|
}
|
|
}
|
|
|
|
void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
if (event->buttons().testFlag(Qt::RightButton)) {
|
|
if ((event->screenPos() - event->buttonDownScreenPos(Qt::RightButton)).manhattanLength() <
|
|
2 * QApplication::startDragDistance()) {
|
|
return;
|
|
}
|
|
|
|
QColor arrowColor = Qt::red;
|
|
if (event->modifiers().testFlag(Qt::ControlModifier)) {
|
|
arrowColor = Qt::yellow;
|
|
} else if (event->modifiers().testFlag(Qt::AltModifier)) {
|
|
arrowColor = Qt::blue;
|
|
} else if (event->modifiers().testFlag(Qt::ShiftModifier)) {
|
|
arrowColor = Qt::green;
|
|
}
|
|
|
|
drawArrow(arrowColor);
|
|
} else if (event->buttons().testFlag(Qt::LeftButton)) {
|
|
if ((event->screenPos() - event->buttonDownScreenPos(Qt::LeftButton)).manhattanLength() <
|
|
2 * QApplication::startDragDistance()) {
|
|
return;
|
|
}
|
|
if (const ZoneViewZoneLogic *view = qobject_cast<const ZoneViewZoneLogic *>(state->getZone())) {
|
|
if (view->getRevealZone() && !view->getWriteableRevealZone()) {
|
|
return;
|
|
}
|
|
} else if (!owner->getPlayerInfo()->getLocalOrJudge()) {
|
|
return;
|
|
}
|
|
|
|
bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier);
|
|
|
|
// Use the buttonDownPos to align the hot spot with the position when
|
|
// the user originally clicked
|
|
createDragItem(state->getId(), event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
|
|
dragItem->grabMouse();
|
|
|
|
int childIndex = 0;
|
|
for (const auto &item : scene()->selectedItems()) {
|
|
CardItem *card = static_cast<CardItem *>(item);
|
|
if ((card == this) || (card->getZone() != state->getZone())) {
|
|
continue;
|
|
}
|
|
++childIndex;
|
|
QPointF childPos;
|
|
if (state->getZone()->getHasCardAttr()) {
|
|
childPos = card->pos() - pos();
|
|
} else {
|
|
childPos = QPointF(childIndex * CardDimensions::WIDTH_HALF_F, 0);
|
|
}
|
|
CardDragItem *drag =
|
|
new CardDragItem(card, card->getId(), childPos, card->getFaceDown() || forceFaceDown, dragItem);
|
|
drag->setPos(dragItem->pos() + childPos);
|
|
scene()->addItem(drag);
|
|
}
|
|
}
|
|
setCursor(Qt::OpenHandCursor);
|
|
}
|
|
|
|
void CardItem::playCard(bool faceDown)
|
|
{
|
|
// Do nothing if the card belongs to another player
|
|
if (!owner->getPlayerInfo()->getLocalOrJudge()) {
|
|
return;
|
|
}
|
|
|
|
TableZoneLogic *tz = qobject_cast<TableZoneLogic *>(state->getZone());
|
|
if (tz) {
|
|
emit tz->toggleTapped();
|
|
} else {
|
|
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
|
if (faceDown) {
|
|
emit playSelectedFaceDown(this);
|
|
} else {
|
|
emit playSelected(this);
|
|
}
|
|
} else {
|
|
state->getZone()->getPlayer()->getPlayerActions()->playCard(state, faceDown);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief returns true if the zone is a unwritable reveal zone view (eg a card reveal window). Will return false if zone
|
|
* is nullptr.
|
|
*/
|
|
static bool isUnwritableRevealZone(CardZoneLogic *zone)
|
|
{
|
|
if (auto *view = qobject_cast<ZoneViewZoneLogic *>(zone)) {
|
|
return view->getRevealZone() && !view->getWriteableRevealZone();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This method is called when a "click to play" is done on the card.
|
|
* This is either triggered by a single click or double click, depending on the settings.
|
|
*
|
|
* @param shiftHeld if the shift key was held during the click
|
|
*/
|
|
void CardItem::handleClickedToPlay(bool shiftHeld)
|
|
{
|
|
if (isUnwritableRevealZone(state->getZone())) {
|
|
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
|
emit hideSelected(this);
|
|
} else {
|
|
state->getZone()->removeCard(state);
|
|
}
|
|
} else {
|
|
playCard(shiftHeld);
|
|
}
|
|
}
|
|
|
|
void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
if (event->button() == Qt::RightButton && owner != nullptr) {
|
|
emit rightClicked(this, event->screenPos());
|
|
return;
|
|
}
|
|
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
|
|
(!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
|
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
|
}
|
|
if (owner != nullptr) {
|
|
setCursor(Qt::OpenHandCursor);
|
|
}
|
|
AbstractCardItem::mouseReleaseEvent(event);
|
|
}
|
|
|
|
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
// The preceding release deferred its multi-selection collapse for exactly this: the double-click plays
|
|
// every selected card, so the collapse must not run.
|
|
cancelPendingSelectionCollapse();
|
|
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
|
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
|
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
|
}
|
|
event->accept();
|
|
}
|
|
|
|
bool CardItem::doubleClickActsOnSelection() const
|
|
{
|
|
return SettingsCache::instance().userInterface().getDoubleClickToPlay();
|
|
}
|
|
|
|
bool CardItem::animationEvent()
|
|
{
|
|
int rotation = ROTATION_DEGREES_PER_FRAME;
|
|
bool animationIncomplete = true;
|
|
if (!state->getTapped()) {
|
|
rotation *= -1;
|
|
}
|
|
|
|
tapAngle += rotation;
|
|
if (state->getTapped() && (tapAngle > 90)) {
|
|
tapAngle = 90;
|
|
animationIncomplete = false;
|
|
}
|
|
if (!state->getTapped() && (tapAngle < 0)) {
|
|
tapAngle = 0;
|
|
animationIncomplete = false;
|
|
}
|
|
|
|
setTransform(QTransform()
|
|
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
|
|
.rotate(tapAngle)
|
|
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
|
|
setHovered(false);
|
|
update();
|
|
|
|
return animationIncomplete;
|
|
}
|
|
|
|
QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value)
|
|
{
|
|
if ((change == ItemSelectedHasChanged) && owner != nullptr) {
|
|
bool selected = value.toBool();
|
|
|
|
if (selected) {
|
|
if (auto *gameScene = qobject_cast<GameScene *>(scene())) {
|
|
gameScene->setActiveCard(this);
|
|
}
|
|
}
|
|
|
|
emit selectionChanged(this, selected);
|
|
}
|
|
|
|
return AbstractCardItem::itemChange(change, value);
|
|
} |