diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game/board/abstract_card_item.cpp index 9ec6ada9a..a519fa4b6 100644 --- a/cockatrice/src/game/board/abstract_card_item.cpp +++ b/cockatrice/src/game/board/abstract_card_item.cpp @@ -85,7 +85,11 @@ const CardInfo &AbstractCardItem::getCardInfo() const void AbstractCardItem::setRealZValue(qreal _zValue) { realZValue = _zValue; - setZValue(_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 @@ -213,8 +217,16 @@ void AbstractCardItem::setHovered(bool _hovered) if (isHovered == _hovered) return; - if (_hovered) + 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().getScaleCards() ? 1.1 : 1); diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h index 7d2c29cae..517b5cf28 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game/board/abstract_card_item.h @@ -1,7 +1,7 @@ /** * @file abstract_card_item.h * @ingroup GameGraphicsCards - * @brief TODO: Document this. + * @brief Base class for graphical card items, providing shared rendering, identity, and interaction logic. */ #ifndef ABSTRACTCARDITEM_H @@ -96,6 +96,10 @@ public: } void setRealZValue(qreal _zValue); void setHovered(bool _hovered); + bool getIsHovered() const + { + return isHovered; + } QString getColor() const { return color; diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 034ff6947..737071fbd 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -5,6 +5,7 @@ #include "phases_toolbar.h" #include "player/player.h" #include "player/player_graphics_item.h" +#include "zones/select_zone.h" #include "zones/view_zone.h" #include "zones/view_zone_widget.h" @@ -344,12 +345,26 @@ void GameScene::updateHover(const QPointF &scenePos) void GameScene::updateHoveredCard(CardItem *newCard) { if (hoveredCard && (newCard != hoveredCard)) - hoveredCard->setHovered(false); + endCardHover(hoveredCard); if (newCard && (newCard != hoveredCard)) - newCard->setHovered(true); + beginCardHover(newCard); hoveredCard = newCard; } +void GameScene::beginCardHover(CardItem *card) +{ + card->setHovered(true); + if (auto *zone = SelectZone::findOwningSelectZone(card)) + zone->escapeClipForHover(card); +} + +void GameScene::endCardHover(CardItem *card) +{ + if (auto *zone = SelectZone::findOwningSelectZone(card)) + zone->restoreClipAfterHover(card); + card->setHovered(false); +} + CardZone *GameScene::findTopmostZone(const QList &items) { for (QGraphicsItem *item : items) @@ -484,6 +499,8 @@ bool GameScene::event(QEvent *event) { if (event->type() == QEvent::GraphicsSceneMouseMove) updateHover(static_cast(event)->scenePos()); + else if (event->type() == QEvent::Leave) + updateHoveredCard(nullptr); return QGraphicsScene::event(event); } diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index f08e83aa4..2fcf3f216 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -56,6 +56,12 @@ private: */ void updateHover(const QPointF &scenePos); + /// Activates hover state and escapes the card from its clip container so hover scaling is visible beyond zone + /// bounds. + void beginCardHover(CardItem *card); + /// Deactivates hover state and restores the card to its clip container. + void endCardHover(CardItem *card); + public: /** * @brief Constructs the GameScene. diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp index 0c189cd2b..cd7ae6a56 100644 --- a/cockatrice/src/game/zones/card_zone.cpp +++ b/cockatrice/src/game/zones/card_zone.cpp @@ -19,6 +19,7 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent) void CardZone::onCardAdded(CardItem *addedCard) { addedCard->setParentItem(this); + addedCard->setVisible(true); addedCard->update(); } diff --git a/cockatrice/src/game/zones/card_zone.h b/cockatrice/src/game/zones/card_zone.h index 6fe8157e4..5df48f7fb 100644 --- a/cockatrice/src/game/zones/card_zone.h +++ b/cockatrice/src/game/zones/card_zone.h @@ -1,7 +1,7 @@ /** * @file card_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Base graphics item for zones that contain cards. */ #ifndef CARDZONE_H @@ -40,7 +40,10 @@ protected: } public slots: bool showContextMenu(const QPoint &screenPos); - void onCardAdded(CardItem *addedCard); + /// @brief Called when a card is added to this zone. Default: reparents card to this item. + /// Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal + /// connection in CardZone's constructor dispatches through the vtable. + virtual void onCardAdded(CardItem *addedCard); public: enum diff --git a/cockatrice/src/game/zones/hand_zone.cpp b/cockatrice/src/game/zones/hand_zone.cpp index 7badfcca4..506a26a80 100644 --- a/cockatrice/src/game/zones/hand_zone.cpp +++ b/cockatrice/src/game/zones/hand_zone.cpp @@ -27,6 +27,10 @@ void HandZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) { + if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) { + return; + } + QPoint point = dropPoint + scenePos().toPoint(); int x = -1; if (SettingsCache::instance().getHorizontalHand()) { @@ -34,9 +38,7 @@ void HandZone::handleDropEvent(const QList &dragItems, if (point.x() < static_cast(getLogic()->getCards().at(x))->scenePos().x()) break; } else { - for (x = 0; x < getLogic()->getCards().size(); x++) - if (point.y() < static_cast(getLogic()->getCards().at(x))->scenePos().y()) - break; + x = calcDropIndexFromY(dropPoint.y()); } Command_MoveCard cmd; @@ -58,7 +60,7 @@ QRectF HandZone::boundingRect() const if (SettingsCache::instance().getHorizontalHand()) return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); else - return QRectF(0, 0, 100, zoneHeight); + return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight); } void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) @@ -78,35 +80,31 @@ void HandZone::reorganizeCards() qreal totalWidth = leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding; - for (int i = 0; i < cardCount; i++) { - CardItem *c = getLogic()->getCards().at(i); - // If the total width of the cards is smaller than the available width, - // the cards do not need to overlap and are displayed in the center of the area. - if (cardWidth * cardCount > totalWidth) - c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5); - else { - qreal xPosition = - leftJustified ? xPadding + ((qreal)i) * cardWidth - : xPadding + ((qreal)i) * cardWidth + (totalWidth - cardCount * cardWidth) / 2; - c->setPos(xPosition, 5); + if (cardCount == 1) { + CardItem *c = getLogic()->getCards().at(0); + qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2; + c->setPos(xPosition, 5); + c->setRealZValue(0); + } else { + for (int i = 0; i < cardCount; i++) { + CardItem *c = getLogic()->getCards().at(i); + // If the total width of the cards is smaller than the available width, + // the cards do not need to overlap and are displayed in the center of the area. + if (cardWidth * cardCount > totalWidth) + c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5); + else { + qreal xPosition = leftJustified ? xPadding + ((qreal)i) * cardWidth + : xPadding + ((qreal)i) * cardWidth + + (totalWidth - cardCount * cardWidth) / 2; + c->setPos(xPosition, 5); + } + c->setRealZValue(i); } - c->setRealZValue(i); } } else { - qreal totalWidth = boundingRect().width(); - qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); - qreal xspace = 5; - qreal x1 = xspace; - qreal x2 = totalWidth - xspace - cardWidth; - - for (int i = 0; i < cardCount; i++) { - CardItem *card = getLogic()->getCards().at(i); - qreal x = (i % 2) ? x2 : x1; - qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(), - getLogic()->getCards().at(0)->boundingRect().height()); - card->setPos(x, y); - card->setRealZValue(i); - } + // No clip container: hand cards should always be visible to the player. + const auto params = buildStackParams(); + layoutCardsVertically(params); } } update(); diff --git a/cockatrice/src/game/zones/hand_zone.h b/cockatrice/src/game/zones/hand_zone.h index 25f4148bd..586302308 100644 --- a/cockatrice/src/game/zones/hand_zone.h +++ b/cockatrice/src/game/zones/hand_zone.h @@ -1,7 +1,7 @@ /** * @file hand_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Graphical zone for the player's hand, supporting horizontal and vertical layouts. */ #ifndef HANDZONE_H @@ -14,7 +14,8 @@ class HandZone : public SelectZone { Q_OBJECT private: - qreal width, zoneHeight; + qreal width = 0.0; + qreal zoneHeight; private slots: void updateBg(); public slots: diff --git a/cockatrice/src/game/zones/select_zone.cpp b/cockatrice/src/game/zones/select_zone.cpp index 9bf5f9faf..5291ea138 100644 --- a/cockatrice/src/game/zones/select_zone.cpp +++ b/cockatrice/src/game/zones/select_zone.cpp @@ -4,38 +4,207 @@ #include "../board/card_item.h" #include "../game_scene.h" -#include +#include #include +#include -qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse) +namespace { - qreal cardMinOverlap = cardHeight * SettingsCache::instance().getStackCardOverlapPercent() / 100; - qreal desiredHeight = cardHeight * cardCount - cardMinOverlap * (cardCount - 1); - qreal y; - if (desiredHeight > totalHeight) { - if (reverse) { - y = index / ((totalHeight - cardHeight) / (cardCount - 1)); +qreal stackingOffset(qreal cardHeight) +{ + const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent(); + return cardHeight * (100.0 - overlapPercent) / 100.0; +} +} // namespace + +SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams ¶ms) +{ + if (params.cardCount <= 0) { + return {0.0, 0.0}; + } + qreal effectiveOffset = params.desiredOffset; + if (params.cardCount > 1) { + qreal fitOffset; + if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) { + // Zone is shorter than a card (e.g. minimized). Compress offsets so + // every card has at least minOffset pixels of its top visible. + fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1); + effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset)); } else { - y = index * (totalHeight - cardHeight) / (cardCount - 1); - } - } else { - qreal start = (totalHeight - desiredHeight) / 2; - if (reverse) { - if (index <= start) { - return 0; + qreal reservedForBottomCard; + if (params.allowBottomOverflow) { + // Allow the bottom card to partially overflow in tight zones, scaling the + // overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly + // as cards are added rather than dropping by 1/(n-1) each time. + // The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow. + constexpr qreal bottomCardZoneRatio = 0.75; + const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast(params.cardCount - 1)); + reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio); + } else { + // No overflow: reserve full card height for the bottom card + reservedForBottomCard = params.cardHeight; } - y = (index - start) / (cardHeight - cardMinOverlap); - } else { - y = index * (cardHeight - cardMinOverlap) + start; + fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1); + effectiveOffset = qMax(params.minOffset, qMin(params.desiredOffset, fitOffset)); } } - return y; + qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight; + qreal start = (stackHeight <= params.totalHeight) ? (params.totalHeight - stackHeight) / 2.0 : 0.0; + return {effectiveOffset, start}; +} + +SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card) +{ + QGraphicsItem *parent = card ? card->parentItem() : nullptr; + if (!parent) + return nullptr; + // Card may be direct child of zone (escaped for hover) or child of clip container. + if (auto *zone = dynamic_cast(parent)) + return zone; + if (auto *zone = dynamic_cast(parent->parentItem())) + return zone; + return nullptr; +} + +SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const +{ + const auto &cards = getLogic()->getCards(); + if (cards.isEmpty()) + return {0, boundingRect().height(), 0.0, 0.0, minOffset}; + const auto cardCount = static_cast(cards.size()); + const qreal cardHeight = cards.at(0)->boundingRect().height(); + const qreal offset = stackingOffset(cardHeight); + return {cardCount, boundingRect().height(), cardHeight, offset, minOffset}; +} + +int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const +{ + const auto &cards = getLogic()->getCards(); + if (cards.isEmpty()) { + return 0; + } + const auto params = buildStackParams(minOffset); + auto [effectiveOffset, start] = computeZoneLayout(params); + if (effectiveOffset <= 0.0) { + return 0; + } + return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1); +} + +void SelectZone::restoreStaleEscapedCards() +{ + if (!cardClipContainer) + return; + for (auto *card : getLogic()->getCards()) { + // A card parented to the zone (instead of the clip container) should + // only occur while it is actively hovered. If hover cleanup was + // missed, reparent it back so clipping resumes. + if (card && card->parentItem() == this && !card->getIsHovered()) { + card->setParentItem(cardClipContainer); + } + } +} + +void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms) +{ + const auto &cards = getLogic()->getCards(); + if (cards.isEmpty() || params.cardCount <= 0) + return; + if (params.cardCount > cards.size()) + return; + + constexpr qreal xspace = 5; + const qreal cardWidth = cards.at(0)->boundingRect().width(); + const qreal totalWidth = boundingRect().width(); + const qreal x1 = xspace; + const qreal x2 = totalWidth - xspace - cardWidth; + const qreal xCentered = (totalWidth - cardWidth) / 2.0; + + auto [effectiveOffset, start] = computeZoneLayout(params); + for (int i = 0; i < params.cardCount; i++) { + CardItem *card = cards.at(i); + qreal y = start + i * effectiveOffset; + // Center single card; alternate left/right for multiple cards + qreal x = (params.cardCount == 1) ? xCentered : ((i % 2) ? x2 : x1); + card->setPos(x, y); + card->setRealZValue(i); + } } SelectZone::SelectZone(CardZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent) { } +SelectZone::~SelectZone() +{ + if (cardClipContainer) { + // Reparent any hover-escaped cards back to the clip container so Qt's + // parent-child tree is consistent for destruction. setParentItem() does + // not invalidate getLogic()->getCards() (it modifies the graphics tree, + // not the zone's logical card list). + for (auto *card : getLogic()->getCards()) { + if (card && card->parentItem() == this) { + card->setParentItem(cardClipContainer); + } + } + } +} + +void SelectZone::onCardAdded(CardItem *addedCard) +{ + if (cardClipContainer && addedCard) { + addedCard->setParentItem(cardClipContainer); + addedCard->setVisible(true); + addedCard->update(); + } else { + CardZone::onCardAdded(addedCard); + } +} + +void SelectZone::setupClipContainer(std::optional zValue) +{ + if (cardClipContainer) + return; + + setFlag(QGraphicsItem::ItemClipsChildrenToShape, false); + + cardClipContainer = new QGraphicsRectItem(this); // Owned by Qt parent-child tree; deleted with this zone. + cardClipContainer->setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); + cardClipContainer->setPen(Qt::NoPen); + cardClipContainer->setBrush(Qt::NoBrush); + cardClipContainer->setRect(boundingRect()); + if (zValue.has_value()) { + cardClipContainer->setZValue(*zValue); + } +} + +void SelectZone::escapeClipForHover(QGraphicsItem *card) +{ + // Reparent from clip container to zone so the hover-scaled card is visible + // beyond clip bounds. Coordinates are identical because the clip container + // is at (0,0) with no transform relative to this zone. + if (cardClipContainer && card && card->parentItem() == cardClipContainer) { + card->setParentItem(this); + cardClipContainer->update(); + } +} + +void SelectZone::restoreClipAfterHover(QGraphicsItem *card) +{ + // Restore card to clip container. If card's parent is not this zone, + // a zone transition already reparented it via onCardAdded — skip. + if (cardClipContainer && card && card->parentItem() == this) { + card->setParentItem(cardClipContainer); + } +} + +void SelectZone::updateClipRect() +{ + if (cardClipContainer) { + cardClipContainer->setRect(boundingRect()); + } +} + void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (event->buttons().testFlag(Qt::LeftButton)) { @@ -56,7 +225,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) continue; } - bool inRect = selectionRect.intersects(card->mapRectToParent(card->boundingRect())); + bool inRect = selectionRect.intersects(card->mapRectToItem(this, card->boundingRect())); if (inRect && !cardsInSelectionRect.contains(card)) { // selection has just expanded to cover the card cardsInSelectionRect.insert(card); diff --git a/cockatrice/src/game/zones/select_zone.h b/cockatrice/src/game/zones/select_zone.h index d6fd3e10e..9f9d9cb6b 100644 --- a/cockatrice/src/game/zones/select_zone.h +++ b/cockatrice/src/game/zones/select_zone.h @@ -1,7 +1,7 @@ /** * @file select_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Base class for zones where cards are laid out and individually interactable. */ #ifndef SELECTZONE_H @@ -10,6 +10,9 @@ #include "card_zone.h" #include +#include + +class QGraphicsRectItem; /** * A CardZone where the cards are laid out, with each card directly interactable by clicking. @@ -17,19 +20,113 @@ class SelectZone : public CardZone { Q_OBJECT +public: + /// Finds the SelectZone that owns a card, regardless of whether the card is parented + /// to the zone directly or to its clip container. Returns nullptr if not in a SelectZone. + static SelectZone *findOwningSelectZone(const QGraphicsItem *card); + + SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr); + ~SelectZone() override; + void onCardAdded(CardItem *addedCard) override; + + /// @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip + /// bounds. Safe no-op if no clip container exists. Coordinates are preserved (clip container is at (0,0) with no + /// transform). + void escapeClipForHover(QGraphicsItem *card); + /// @brief Restores a hover-escaped card back to the clip container. Guards against zone transitions that already + /// reparented the card. + void restoreClipAfterHover(QGraphicsItem *card); + private: QPointF selectionOrigin; QSet cardsInSelectionRect; + /// Invisible clipping parent for cards; owned by Qt parent-child tree (parented to this zone). + /// Created by setupClipContainer(); null when no clip container is active. + QGraphicsRectItem *cardClipContainer = nullptr; protected: + // -- Layout computation -- + + /// Parameters describing a vertical card stack's geometry. + struct StackLayoutParams + { + int cardCount; + qreal totalHeight; + qreal cardHeight; + qreal desiredOffset; + qreal minOffset = 0.0; + /// When false (default), reserves full cardHeight for the bottom card, ensuring + /// all cards remain within zone bounds. When true, allows the bottom card to + /// partially overflow using sqrt-scaled allowance. Use with setupClipContainer() + /// for zones too short to fit a full card. + bool allowBottomOverflow = false; + }; + + /// Result of computing a vertical stack layout. + struct ZoneLayout + { + qreal effectiveOffset; ///< Actual offset between card tops (may be compressed) + qreal start; ///< Y coordinate of the first card's top edge + }; + + /// Minimum visible pixels of each card's top edge when stacking compresses offsets in tight zones. + static constexpr qreal MIN_CARD_VISIBLE = 10.0; + + /** + * @brief Computes layout for a vertical card stack (effective offset and start position). + * + * Three regimes: + * 1. Minimized zone (totalHeight < card height with minOffset > 0): offsets compress + * so each card retains at least minOffset visible pixels of its top edge. + * 2. Normal zone with allowBottomOverflow=false (default): the bottom card is + * guaranteed to fit within the zone boundary. Offsets compress as needed. + * 3. Normal zone with allowBottomOverflow=true: the bottom card may partially + * overflow. The overflow allowance is scaled by sqrt(cardCount-1) so that + * adding one card shifts existing cards smoothly. + * + * When the stack fits with room to spare, it is centered vertically. + */ + static ZoneLayout computeZoneLayout(const StackLayoutParams ¶ms); + + /// Builds StackLayoutParams from the current card list and zone geometry. + StackLayoutParams buildStackParams(qreal minOffset = 0.0) const; + + /// Computes the card index at a given y-coordinate within the zone's vertical layout. + /// Returns 0 if the zone has no cards or the offset is zero. + int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const; + + /** + * @brief Positions cards vertically with alternating left/right x-offsets. + * + * Cards alternate between left and right margins (5px padding from zone edges): + * even-indexed cards at left, odd-indexed at right. + * Cards are assigned ascending z-values. + * + * @param params Stack layout geometry parameters (use allowBottomOverflow to control overflow) + */ + void layoutCardsVertically(const StackLayoutParams ¶ms); + + // -- Clip container -- + // The clip container mechanism is available for future zones that need visual clipping + // (e.g., zones too short to fit a full card). To enable: call setupClipContainer() in the + // zone's constructor, and set allowBottomOverflow=true in layout params. + + /// Restores any cards that were hover-escaped but whose hover state was not properly cleaned up. + /// Call at the start of reorganizeCards() in zones that use a clip container. + void restoreStaleEscapedCards(); + + /// Creates a clip container child item that clips card overflow to zone bounds. + /// Cards entering this zone are reparented to this container by the onCardAdded override. + /// Disables zone-level child clipping; clipping is delegated to the container. + /// @param zValue Optional z-value for the clip container (e.g. ZValues::CARD_BASE) + void setupClipContainer(std::optional zValue = std::nullopt); + + /// Updates the clip container rect to match this zone's current boundingRect(). + void updateClipRect(); + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; - -public: - SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr); }; -qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse = false); - #endif diff --git a/cockatrice/src/game/zones/stack_zone.cpp b/cockatrice/src/game/zones/stack_zone.cpp index a9d649a4a..c0da0bfd2 100644 --- a/cockatrice/src/game/zones/stack_zone.cpp +++ b/cockatrice/src/game/zones/stack_zone.cpp @@ -1,10 +1,9 @@ #include "stack_zone.h" -#include "../../client/settings/cache_settings.h" #include "../../interface/theme_manager.h" -#include "../board/arrow_item.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" +#include "../card_dimensions.h" #include "../player/player.h" #include "../player/player_actions.h" #include "logic/stack_zone_logic.h" @@ -27,7 +26,7 @@ void StackZone::updateBg() QRectF StackZone::boundingRect() const { - return {0, 0, 100, zoneHeight}; + return {0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight}; } void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) @@ -40,7 +39,15 @@ void StackZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) { - if (startZone == nullptr || startZone->getPlayer() == nullptr) { + if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) { + return; + } + + int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE); + + // Same-zone no-op: don't move a card onto itself + const auto &cards = getLogic()->getCards(); + if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) { return; } @@ -49,36 +56,12 @@ void StackZone::handleDropEvent(const QList &dragItems, cmd.set_start_zone(startZone->getName().toStdString()); cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); cmd.set_target_zone(getLogic()->getName().toStdString()); - - int index = 0; - - if (!getLogic()->getCards().isEmpty()) { - const auto cardCount = static_cast(getLogic()->getCards().size()); - const auto &card = getLogic()->getCards().at(0); - - index = qRound(divideCardSpaceInZone(dropPoint.y(), cardCount, boundingRect().height(), - card->boundingRect().height(), true)); - - // divideCardSpaceInZone is not guaranteed to return a valid index - // currently, so clamp it to avoid crashes. - index = qBound(0, index, cardCount - 1); - - if (startZone == getLogic()) { - const auto &dragItem = dragItems.at(0); - const auto &card = getLogic()->getCards().at(index); - - if (card->getId() == dragItem->getId()) { - return; - } - } - } - cmd.set_x(index); cmd.set_y(0); - for (CardDragItem *item : dragItems) { + for (const CardDragItem *item : dragItems) { if (item) { - auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); cardToMove->set_card_id(item->getId()); if (item->isForceFaceDown()) { cardToMove->set_face_down(true); @@ -89,24 +72,22 @@ void StackZone::handleDropEvent(const QList &dragItems, getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } +void StackZone::setHeight(qreal newHeight) +{ + if (qFuzzyCompare(1.0 + zoneHeight, 1.0 + newHeight)) { + return; + } + prepareGeometryChange(); + zoneHeight = newHeight; + reorganizeCards(); + update(); +} + void StackZone::reorganizeCards() { if (!getLogic()->getCards().isEmpty()) { - const auto cardCount = static_cast(getLogic()->getCards().size()); - qreal totalWidth = boundingRect().width(); - qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); - qreal xspace = 5; - qreal x1 = xspace; - qreal x2 = totalWidth - xspace - cardWidth; - - for (int i = 0; i < cardCount; i++) { - CardItem *card = getLogic()->getCards().at(i); - qreal x = (i % 2) ? x2 : x1; - qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(), - getLogic()->getCards().at(0)->boundingRect().height()); - card->setPos(x, y); - card->setRealZValue(i); - } + const auto params = buildStackParams(MIN_CARD_VISIBLE); + layoutCardsVertically(params); } update(); } diff --git a/cockatrice/src/game/zones/stack_zone.h b/cockatrice/src/game/zones/stack_zone.h index 7c98f5128..8f5bed09d 100644 --- a/cockatrice/src/game/zones/stack_zone.h +++ b/cockatrice/src/game/zones/stack_zone.h @@ -1,7 +1,7 @@ /** * @file stack_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Graphical zone for the stack, displaying cards in a vertical pile. */ #ifndef STACKZONE_H @@ -20,6 +20,8 @@ private slots: public: StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent); + /// @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. + void setHeight(qreal newHeight); void handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override; QRectF boundingRect() const override;