[Game] Add Command Zone support with commander tax tracking

- Add CommandZone and CommandZoneLogic for commander
  - Add CommanderTaxCounter
  - Add counter active state protocol (show/hide tax counters)
  - Add "Enable Command Zone" option in game creation dialogs
  - Add context menu actions for command zone operations

Took 9 minutes

Took 11 minutes
This commit is contained in:
DawnFire42 2026-05-21 21:30:40 -04:00
parent 18b23b19a7
commit b4adb20a5f
No known key found for this signature in database
GPG key ID: 24BB855EE2911B33
69 changed files with 1506 additions and 52 deletions

View file

@ -0,0 +1,60 @@
#include "commander_tax_counter.h"
#include "counter_state.h"
#include "translate_counter_name.h"
#include <QColor>
#include <QFontDatabase>
#include <QPainter>
static constexpr qreal CORNER_RADIUS = 4.0;
static constexpr qreal FONT_SIZE_RATIO = 0.6;
static constexpr int OVERLAY_ALPHA = 191;
static const QColor OVERLAY_BG_NORMAL{40, 40, 40, OVERLAY_ALPHA};
static const QColor OVERLAY_BG_HOVERED{70, 70, 70, OVERLAY_ALPHA};
CommanderTaxCounter::CommanderTaxCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent)
: AbstractCounter(state, player, false, false, parent), size(TaxCounterSizes::TAX_COUNTER_SIZE)
{
setCacheMode(DeviceCoordinateCache);
setAcceptHoverEvents(true);
setCursor(Qt::ArrowCursor);
setToolTip(tr("%1: %2").arg(TranslateCounterName::getDisplayName(getName())).arg(getValue()));
}
QRectF CommanderTaxCounter::boundingRect() const
{
return QRectF(0, 0, size, size);
}
void CommanderTaxCounter::paint(QPainter *painter,
[[maybe_unused]] const QStyleOptionGraphicsItem *option,
[[maybe_unused]] QWidget *widget)
{
painter->save();
QRectF rect = boundingRect().adjusted(1, 1, -1, -1);
QColor bgColor = hovered ? OVERLAY_BG_HOVERED : OVERLAY_BG_NORMAL;
painter->setPen(Qt::NoPen);
painter->setBrush(bgColor);
painter->drawRoundedRect(rect, CORNER_RADIUS, CORNER_RADIUS);
QFont f = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
f.setPixelSize(static_cast<int>(size * FONT_SIZE_RATIO));
f.setWeight(QFont::Bold);
painter->setFont(f);
painter->setPen(Qt::white);
painter->drawText(rect, Qt::AlignCenter, QString::number(value));
painter->restore();
}
void CommanderTaxCounter::setValue(int _value)
{
int clampedValue = qMax(0, _value);
AbstractCounter::setValue(clampedValue);
setToolTip(tr("%1: %2").arg(TranslateCounterName::getDisplayName(getName())).arg(clampedValue));
}

View file

@ -0,0 +1,72 @@
/**
* @file commander_tax_counter.h
* @ingroup GameGraphicsPlayers
* @brief Square counter for commander tax, clamped to non-negative values.
*/
#ifndef COCKATRICE_COMMANDER_TAX_COUNTER_H
#define COCKATRICE_COMMANDER_TAX_COUNTER_H
#include "abstract_counter.h"
/**
* @namespace TaxCounterSizes
* @brief Size constants for commander tax counter layout.
*/
namespace TaxCounterSizes
{
/** @brief Size of commander tax counter icons (width and height) */
constexpr int TAX_COUNTER_SIZE = 24;
/** @brief Margin around and between tax counter icons */
constexpr int TAX_COUNTER_MARGIN = 2;
} // namespace TaxCounterSizes
/**
* @class CommanderTaxCounter
* @brief Counter for tracking commander tax in Commander format.
*
* Displays cumulative cost increase for casting a commander. The counter
* is manually adjusted by the player to track their commander tax. Values
* are clamped to >= 0.
*
* Appearance: square with rounded corners, semi-transparent background,
* positioned at top-left of command zone.
*
* Two instances per player: CounterIds::CommanderTax and CounterIds::PartnerTax.
* Each counter supports an active/inactive state (inherited from AbstractCounter):
* commander tax starts active; partner tax starts inactive until explicitly
* enabled by the player via the context menu.
*
* @see AbstractCounter
* @see AbstractCounter::setActive()
* @see CounterIds
*/
class CommanderTaxCounter : public AbstractCounter
{
Q_OBJECT
private:
int size;
public:
/**
* @brief Constructs a CommanderTaxCounter.
* @param state Counter state containing id, name, value, etc.
* @param player The player who owns this counter
* @param parent Parent graphics item (typically the command zone)
*/
CommanderTaxCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent = nullptr);
[[nodiscard]] QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
/**
* @brief Overrides AbstractCounter::setValue to clamp values to >= 0 and update the tooltip.
* @param _value New value (clamped if negative)
*/
void setValue(int _value) override;
};
#endif // COCKATRICE_COMMANDER_TAX_COUNTER_H

View file

@ -2,15 +2,22 @@
#include <libcockatrice/utility/color.h>
CounterState::CounterState(int id, const QString &name, const QColor &color, int radius, int value, QObject *parent)
: QObject(parent), id(id), name(name), color(color), radius(radius), value(value)
CounterState::CounterState(int id,
const QString &name,
const QColor &color,
int radius,
int value,
bool active,
QObject *parent)
: QObject(parent), id(id), name(name), color(color), radius(radius), value(value), active(active)
{
}
CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject *parent)
{
return new CounterState(counter.id(), QString::fromStdString(counter.name()),
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(),
counter.active(), parent);
}
void CounterState::setValue(int newValue)
@ -21,4 +28,13 @@ void CounterState::setValue(int newValue)
int old = value;
value = newValue;
emit valueChanged(old, newValue);
}
void CounterState::setActive(bool newActive)
{
if (newActive == active) {
return;
}
active = newActive;
emit activeChanged(newActive);
}

View file

@ -10,7 +10,13 @@ class CounterState : public QObject
{
Q_OBJECT
public:
CounterState(int id, const QString &name, const QColor &color, int radius, int value, QObject *parent = nullptr);
CounterState(int id,
const QString &name,
const QColor &color,
int radius,
int value,
bool active = true,
QObject *parent = nullptr);
static CounterState *fromProto(const ServerInfo_Counter &counter, QObject *parent = nullptr);
@ -34,11 +40,17 @@ public:
{
return value;
}
bool isActive() const
{
return active;
}
void setValue(int newValue);
void setActive(bool newActive);
signals:
void valueChanged(int oldValue, int newValue);
void activeChanged(bool newActive);
private:
int id;
@ -46,6 +58,7 @@ private:
QColor color;
int radius;
int value;
bool active;
};
#endif // COCKATRICE_COUNTER_STATE_H

View file

@ -7,6 +7,7 @@
#include "../../game_graphics/zones/table_zone.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include "../zones/view_zone_logic.h"
#include <libcockatrice/card/database/card_database_manager.h>
@ -24,10 +25,12 @@
#include <libcockatrice/protocol/pb/command_roll_die.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter_active.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/expression.h>
#include <libcockatrice/utility/zone_names.h>
@ -1634,6 +1637,14 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
void PlayerActions::playSelectedCards(QList<CardItem *> selectedCards, const bool faceDown)
{
playSelectedCardsImpl(faceDown, nullptr);
}
void PlayerActions::playSelectedCardsImpl(bool faceDown,
const std::function<void(CardItem *, const QString &)> &postPlayCallback)
{
QList<CardItem *> selectedCards = player->getGameScene()->selectedCards();
// CardIds will get shuffled downwards when cards leave the deck.
// We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play
// out the cards one-by-one.
@ -1642,11 +1653,68 @@ void PlayerActions::playSelectedCards(QList<CardItem *> selectedCards, const boo
for (auto &card : selectedCards) {
if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != ZoneNames::TABLE) {
const QString originalZone = card->getZone()->getName();
playCard(card, faceDown);
if (postPlayCallback) {
postPlayCallback(card, originalZone);
}
}
}
}
void PlayerActions::actPlayAndIncreaseTax()
{
playSelectedCardsImpl(false, [this](CardItem * /*card*/, const QString &originalZone) {
if (originalZone == ZoneNames::COMMAND) {
AbstractCounter *ctr = player->getCounterWidget(CounterIds::CommanderTax);
if (ctr && ctr->isActive()) {
sendIncCounter(CounterIds::CommanderTax, 2);
}
}
});
}
void PlayerActions::actPlayAndIncreasePartnerTax()
{
playSelectedCardsImpl(false, [this](CardItem * /*card*/, const QString &originalZone) {
if (originalZone == ZoneNames::COMMAND) {
AbstractCounter *ctr = player->getCounterWidget(CounterIds::PartnerTax);
if (ctr && ctr->isActive()) {
sendIncCounter(CounterIds::PartnerTax, 2);
}
}
});
}
void PlayerActions::sendIncCounter(int counterId, int delta)
{
Command_IncCounter cmd;
cmd.set_counter_id(counterId);
cmd.set_delta(delta);
sendGameCommand(cmd);
}
void PlayerActions::actModifyTaxCounter(int counterId, int delta)
{
AbstractCounter *ctr = player->getCounterWidget(counterId);
if (!ctr || !ctr->isActive()) {
return;
}
sendIncCounter(counterId, delta);
}
void PlayerActions::actToggleTaxCounter(int counterId)
{
AbstractCounter *ctr = player->getCounterWidget(counterId);
if (!ctr || (ctr->isActive() && ctr->getValue() != 0)) {
return;
}
Command_SetCounterActive cmd;
cmd.set_counter_id(counterId);
cmd.set_active(!ctr->isActive());
sendGameCommand(cmd);
}
void PlayerActions::actPlay(QList<CardItem *> selectedCards)
{
playSelectedCards(selectedCards, false);
@ -1927,6 +1995,18 @@ void PlayerActions::cardMenuAction(QList<CardItem *> selectedCards, CardMenuActi
commandList.append(cmd);
break;
}
case cmMoveToCommandZone: {
auto *cmd = new Command_MoveCard;
cmd->set_start_player_id(startPlayerId);
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone(ZoneNames::COMMAND);
cmd->set_x(0);
cmd->set_y(0);
commandList.append(cmd);
break;
}
case cmMoveToTable: {
// Each card needs its own command because table row, pt, and cipt vary per card
for (const auto &card : cardList) {

View file

@ -17,6 +17,7 @@
#include <QMenu>
#include <QObject>
#include <functional>
#include <libcockatrice/card/relation/card_relation_type.h>
#include <libcockatrice/filters/filter_string.h>
@ -126,6 +127,14 @@ public slots:
void actPlay(QList<CardItem *> selectedCards);
void actPlayFacedown(QList<CardItem *> selectedCards);
/** @brief Plays the selected card and increments the primary commander tax counter. */
void actPlayAndIncreaseTax();
/** @brief Plays the selected card and increments the partner commander tax counter. */
void actPlayAndIncreasePartnerTax();
/** @brief Modifies a tax counter by delta if it is active. */
void actModifyTaxCounter(int counterId, int delta);
/** @brief Toggles a tax counter's active state (only if inactive or value is 0). */
void actToggleTaxCounter(int counterId);
void actHide(QList<CardItem *> selectedCards);
void actMoveTopCardToPlay();
@ -219,6 +228,8 @@ public slots:
void cardMenuAction(QList<CardItem *> selectedCards, CardMenuActionType type);
private:
void sendIncCounter(int counterId, int delta);
PlayerLogic *player;
int defaultNumberTopCards = 1;
@ -245,6 +256,14 @@ private:
void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false);
/**
* @brief Shared implementation for playing selected cards with an optional post-play callback.
* @param postPlayCallback Called after each card is played, receiving the card and its *original* zone name
* (captured before playCard, since playCard sends a move command that may change the card's zone).
*/
void playSelectedCardsImpl(bool faceDown,
const std::function<void(CardItem *, const QString &)> &postPlayCallback = nullptr);
void cmdSetTopCard(Command_MoveCard &cmd);
void cmdSetBottomCard(Command_MoveCard &cmd);

View file

@ -4,6 +4,7 @@
#include "../../game_graphics/board/card_item.h"
#include "../../game_graphics/zones/view_zone.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_counter.h"
#include "../board/arrow_data.h"
#include "../board/card_list.h"
#include "player_actions.h"
@ -31,8 +32,10 @@
#include <libcockatrice/protocol/pb/event_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/event_set_counter.pb.h>
#include <libcockatrice/protocol/pb/event_set_counter_active.pb.h>
#include <libcockatrice/protocol/pb/event_shuffle.pb.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/zone_names.h>
PlayerEventHandler::PlayerEventHandler(PlayerLogic *_player) : QObject(_player), player(_player)
@ -264,13 +267,31 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
{
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
if (!ctr) {
CounterState *state = player->getCounters().value(event.counter_id(), nullptr);
if (!state) {
return;
}
int oldValue = ctr->getValue();
ctr->setValue(event.value());
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
int oldValue = state->getValue();
state->setValue(event.value());
if (event.value() != oldValue) {
emit logSetCounter(player, state->getName(), event.value(), oldValue);
}
}
void PlayerEventHandler::eventSetCounterActive(const Event_SetCounterActive &event)
{
CounterState *state = player->getCounters().value(event.counter_id(), nullptr);
if (!state) {
return;
}
state->setActive(event.active());
AbstractCounter *widget = player->getGraphicsItem()->getCounterWidget(event.counter_id());
if (widget) {
widget->setActive(event.active());
emit player->rearrangeCounters();
}
}
void PlayerEventHandler::eventDelCounter(const Event_DelCounter &event)
@ -627,6 +648,9 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
case GameEvent::SET_COUNTER:
eventSetCounter(event.GetExtension(Event_SetCounter::ext));
break;
case GameEvent::SET_COUNTER_ACTIVE:
eventSetCounterActive(event.GetExtension(Event_SetCounterActive::ext));
break;
case GameEvent::DEL_COUNTER:
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
break;

View file

@ -34,6 +34,7 @@ class Event_RollDie;
class Event_SetCardAttr;
class Event_SetCardCounter;
class Event_SetCounter;
class Event_SetCounterActive;
class Event_Shuffle;
class Event_GameLogNotice;
@ -104,6 +105,7 @@ public:
void eventSetCardCounter(const Event_SetCardCounter &event);
void eventCreateCounter(const Event_CreateCounter &event);
void eventSetCounter(const Event_SetCounter &event);
void eventSetCounterActive(const Event_SetCounterActive &event);
void eventDelCounter(const Event_DelCounter &event);
void eventDumpZone(const Event_DumpZone &event);
void eventMoveCard(const Event_MoveCard &event, const GameEventContext &context);

View file

@ -12,6 +12,10 @@
#include "../../interface/theme_manager.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../board/card_list.h"
#include "../board/commander_tax_counter.h"
#include "../board/counter_general.h"
#include "../game_scene.h"
#include "../zones/command_zone.h"
#include "player_actions.h"
#include <QDebug>
@ -28,11 +32,12 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_zone.pb.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/counter_ids.h>
PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent)
: QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)),
playerEventHandler(new PlayerEventHandler(this)), playerActions(new PlayerActions(this)), active(false),
conceded(false), zoneId(0), dialogSemaphore(false)
conceded(false), zoneId(0), dialogSemaphore(false), serverHasCommandZone(false)
{
initializeZones();
}
@ -48,6 +53,7 @@ void PlayerLogic::initializeZones()
bool visibleHand = playerInfo->getLocalOrJudge() ||
(game->getPlayerManager()->isSpectator() && game->getGameMetaInfo()->spectatorsOmniscient());
addZone(new HandZoneLogic(this, ZoneNames::HAND, false, false, visibleHand, this));
addZone(new CommandZoneLogic(this, ZoneNames::COMMAND, true, false, true, this));
}
PlayerLogic::~PlayerLogic()
@ -104,7 +110,9 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
/* StackZone */
ZoneNames::STACK,
/* HandZone */
ZoneNames::HAND};
ZoneNames::HAND,
/* CommandZone */
ZoneNames::COMMAND};
clearCounters();
emit arrowsClearedLocally();
@ -119,7 +127,19 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
emit clearCustomZonesMenu();
// Check if server has command zone by scanning the zone list
const int zoneListSize = info.zone_list_size();
bool foundCommandZone = false;
for (int i = 0; i < zoneListSize; ++i) {
if (QString::fromStdString(info.zone_list(i).name()) == ZoneNames::COMMAND) {
foundCommandZone = true;
break;
}
}
if (serverHasCommandZone != foundCommandZone) {
serverHasCommandZone = foundCommandZone;
emit commandZoneSupportChanged(foundCommandZone);
}
for (int i = 0; i < zoneListSize; ++i) {
const ServerInfo_Zone &zoneInfo = info.zone_list(i);
@ -253,15 +273,17 @@ void PlayerLogic::setDeck(const DeckList &_deck)
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
{
return addCounter(counter.id(), QString::fromStdString(counter.name()),
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count());
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(),
counter.active());
}
CounterState *PlayerLogic::addCounter(int id, const QString &name, const QColor &color, int radius, int value)
CounterState *
PlayerLogic::addCounter(int id, const QString &name, const QColor &color, int radius, int value, bool active)
{
if (counters.contains(id)) {
return nullptr;
}
auto *state = new CounterState(id, name, color, radius, value, this);
auto *state = new CounterState(id, name, color, radius, value, active, this);
counters.insert(id, state);
emit counterAdded(state);
return state;
@ -296,6 +318,11 @@ CounterState *PlayerLogic::getLifeCounter() const
return nullptr;
}
AbstractCounter *PlayerLogic::getCounterWidget(int counterId) const
{
return graphicsItem->getCounterWidget(counterId);
}
bool PlayerLogic::clearCardsToDelete()
{
if (cardsToDelete.isEmpty()) {

View file

@ -11,6 +11,7 @@
#include "../../interface/widgets/menus/tearoff_menu.h"
#include "../board/arrow_data.h"
#include "../interface/deck_loader/loaded_deck.h"
#include "../zones/command_zone_logic.h"
#include "../zones/hand_zone_logic.h"
#include "../zones/pile_zone_logic.h"
#include "../zones/stack_zone_logic.h"
@ -57,6 +58,7 @@ class ServerInfo_Counter;
class ServerInfo_Player;
class ServerInfo_User;
class TabGame;
class AbstractCounter;
const int MAX_TOKENS_PER_DIALOG = 99;
@ -87,6 +89,7 @@ signals:
void arrowDeleteRequested(int creatorId, int arrowId);
void arrowDeleted(int creatorId, int arrowId);
void arrowsClearedLocally(); // fires on clear() and processPlayerInfo
void commandZoneSupportChanged(bool hasCommandZone);
public slots:
void setActive(bool _active);
@ -191,8 +194,21 @@ public:
return qobject_cast<HandZoneLogic *>(zones.value(ZoneNames::HAND));
}
/** @brief Returns the command zone logic, or nullptr if not present. */
CommandZoneLogic *getCommandZone()
{
return qobject_cast<CommandZoneLogic *>(zones.value(ZoneNames::COMMAND));
}
/** @brief Whether the server confirmed command zone support for this game. */
bool hasServerCommandZone() const
{
return serverHasCommandZone;
}
CounterState *addCounter(const ServerInfo_Counter &counter);
CounterState *addCounter(int id, const QString &name, const QColor &color, int radius, int value);
CounterState *
addCounter(int id, const QString &name, const QColor &color, int radius, int value, bool active = true);
void delCounter(int counterId);
void clearCounters();
@ -206,6 +222,9 @@ public:
*/
CounterState *getLifeCounter() const;
/** @brief Returns the counter widget for the given ID, or nullptr if not found. */
AbstractCounter *getCounterWidget(int counterId) const;
void setConceded(bool _conceded);
bool getConceded() const
{
@ -242,6 +261,7 @@ private:
QMap<int, CounterState *> counters;
bool dialogSemaphore;
bool serverHasCommandZone;
QList<CardItem *> cardsToDelete;
};

View file

@ -202,6 +202,9 @@ QString CardZoneLogic::getTranslatedName(bool theirOwn, GrammaticalCase gc) cons
return (theirOwn ? tr("their graveyard", "nominative") : tr("%1's graveyard", "nominative").arg(ownerName));
} else if (name == ZoneNames::EXILE) {
return (theirOwn ? tr("their exile", "nominative") : tr("%1's exile", "nominative").arg(ownerName));
} else if (name == ZoneNames::COMMAND) {
return (theirOwn ? tr("their command zone", "nominative")
: tr("%1's command zone", "nominative").arg(ownerName));
} else if (name == ZoneNames::SIDEBOARD) {
switch (gc) {
case CaseLookAtZone:

View file

@ -0,0 +1,176 @@
#include "command_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game_graphics/zones/select_zone.h"
#include "../../interface/theme_manager.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
#include "../board/commander_tax_counter.h"
#include "../player/player_actions.h"
#include "../player/player_logic.h"
#include "../z_values.h"
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/utility/counter_ids.h>
CommandZone::CommandZone(CommandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
: SelectZone(_logic, parent), zoneHeight(_zoneHeight)
{
connect(themeManager, &ThemeManager::themeChanged, this, &CommandZone::updateBg);
updateBg();
setCacheMode(DeviceCoordinateCache);
setupClipContainer(ZValues::CARD_BASE);
}
void CommandZone::updateBg()
{
update();
}
QRectF CommandZone::boundingRect() const
{
return {0, 0, ZoneSizes::COMMAND_ZONE_WIDTH, currentHeight()};
}
qreal CommandZone::currentHeight() const
{
return minimized ? qMax(zoneHeight * MINIMIZED_HEIGHT_RATIO, static_cast<double>(minimumHeight)) : zoneHeight;
}
void CommandZone::setMinimumHeight(int height)
{
if (minimumHeight == height) {
return;
}
minimumHeight = height;
prepareGeometryChange();
updateClipRect();
reorganizeCards();
update();
// NOTE: Do NOT emit minimizedChanged here. The minimized STATE has not changed,
// only the minimum height constraint. Emitting here causes an infinite loop:
// rearrangeZones -> rearrangeCounters -> rearrangeTaxCounters -> setMinimumHeight
// -> minimizedChanged -> rearrangeZones (loop!)
}
bool CommandZone::isMinimized() const
{
return minimized;
}
void CommandZone::toggleMinimized()
{
minimized = !minimized;
prepareGeometryChange();
updateClipRect();
reorganizeCards();
update();
emit minimizedChanged(minimized);
}
void CommandZone::paint(QPainter *painter,
[[maybe_unused]] const QStyleOptionGraphicsItem *option,
[[maybe_unused]] QWidget *widget)
{
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Command, getLogic()->getPlayer()->getZoneId());
QPointF scenePos = mapToScene(QPointF(0, 0));
painter->setBrushOrigin(-scenePos);
painter->fillRect(boundingRect(), brush);
}
void CommandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
CardZoneLogic *startZone,
const QPoint &dropPoint)
{
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;
}
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(index);
cmd.set_y(0);
for (const CardDragItem *item : dragItems) {
if (item) {
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(item->getId());
if (item->isForceFaceDown()) {
cardToMove->set_face_down(true);
}
}
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
}
void CommandZone::reorganizeCards()
{
restoreStaleEscapedCards();
updateClipRect();
const auto &cards = getLogic()->getCards();
if (cards.isEmpty()) {
update();
return;
}
auto params = buildStackParams(MIN_CARD_VISIBLE);
params.allowBottomOverflow = true;
layoutCardsVertically(params);
update();
}
void CommandZone::rearrangeTaxCounters()
{
bool commandZoneVisible = isVisible();
int activeTaxCounterCount = 0;
auto *graphicsItem = getLogic()->getPlayer()->getGraphicsItem();
if (!graphicsItem) {
return;
}
for (AbstractCounter *ctr : graphicsItem->getTaxCounterWidgets()) {
qreal y = TaxCounterSizes::TAX_COUNTER_MARGIN +
activeTaxCounterCount * (TaxCounterSizes::TAX_COUNTER_SIZE + TaxCounterSizes::TAX_COUNTER_MARGIN);
ctr->setPos(TaxCounterSizes::TAX_COUNTER_MARGIN, y);
ctr->setZValue(ZValues::TAX_COUNTERS);
bool visible = commandZoneVisible && ctr->isActive();
ctr->setVisible(visible);
if (visible) {
++activeTaxCounterCount;
}
}
int minHeight = activeTaxCounterCount * (TaxCounterSizes::TAX_COUNTER_SIZE + TaxCounterSizes::TAX_COUNTER_MARGIN) +
TaxCounterSizes::TAX_COUNTER_MARGIN;
setMinimumHeight(minHeight);
}
void CommandZone::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
toggleMinimized();
event->accept();
} else {
SelectZone::mouseDoubleClickEvent(event);
}
}

View file

@ -0,0 +1,103 @@
/**
* @file command_zone.h
* @ingroup GameGraphicsZones
* @brief Graphics layer for the command zone, used for Commander format.
*/
#ifndef COCKATRICE_COMMAND_ZONE_H
#define COCKATRICE_COMMAND_ZONE_H
#include "../../game_graphics/zones/select_zone.h"
#include "../card_dimensions.h"
#include "command_zone_logic.h"
#include <QLoggingCategory>
inline Q_LOGGING_CATEGORY(CommandZoneLog, "command_zone");
/**
* @namespace ZoneSizes
* @brief Size constants for the command zone and its sub-elements.
*/
namespace ZoneSizes
{
/** @brief Height of the command zone (accommodates a card plus padding) */
constexpr qreal COMMAND_ZONE_HEIGHT = CardDimensions::HEIGHT + 8;
/** @brief Width of the command zone (matches stack zone) */
constexpr qreal COMMAND_ZONE_WIDTH = CardDimensions::WIDTH_F * 1.5;
} // namespace ZoneSizes
/**
* @class CommandZone
* @brief Graphics layer for the command zone in Commander format games.
*
* Always visible when enabled. Supports multiple cards using a zigzag
* horizontal stacking pattern: single cards display centered, multiple
* cards alternate left-right with vertical overlap compression.
* Can be minimized to 25% height via double-click.
*
* @see CommandZoneLogic for card data management
* @see CommanderTaxCounter for the tax counter overlay
*/
class CommandZone : public SelectZone
{
Q_OBJECT
public:
static constexpr qreal MINIMUM_STACKING_HEIGHT = 50.0;
private:
static constexpr double MINIMIZED_HEIGHT_RATIO = 0.25;
int zoneHeight; ///< Full height in pixels when expanded
bool minimized = false; ///< Whether zone is at 25% height
int minimumHeight = 0; ///< Floor for minimized height (e.g. to fit tax counters)
public:
/**
* @brief Constructs a CommandZone graphics item.
* @param _logic Logic layer managing card data
* @param _zoneHeight Zone height in pixels
* @param parent Parent graphics item
*/
CommandZone(CommandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
/**
* @brief Handles card drops, calculating insertion position from drop point.
* @param dragItems Cards being dragged
* @param startZone Source zone
* @param dropPoint Drop position in local coordinates
*/
void
handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;
/** @brief Returns the bounding rectangle, accounting for minimized state. */
[[nodiscard]] QRectF boundingRect() const override;
/** @brief Paints the zone background using the Commander theme brush. */
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
/** @brief Repositions cards using zigzag horizontal stacking with overlap compression. */
void reorganizeCards() override;
/** @brief Toggles between full and 25% minimized height. */
void toggleMinimized();
[[nodiscard]] bool isMinimized() const;
/** @brief Returns the current display height (full or minimized). */
[[nodiscard]] qreal currentHeight() const;
/** @brief Sets the minimum height floor, e.g. to ensure tax counters remain visible. */
void setMinimumHeight(int height);
/** @brief Lays out visible tax counters vertically in the top-left corner of the command zone. */
void rearrangeTaxCounters();
signals:
/** @brief Emitted when the zone toggles between minimized and expanded states. */
void minimizedChanged(bool isMinimized);
protected:
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
private slots:
void updateBg();
};
#endif // COCKATRICE_COMMAND_ZONE_H

View file

@ -0,0 +1,19 @@
#include "command_zone_logic.h"
#include "../board/card_item.h"
#include "card_zone_algorithms.h"
CommandZoneLogic::CommandZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: CardZoneLogic(_player, _name, _hasCardAttr, _isShufflable, _contentsKnown, parent)
{
}
void CommandZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x, false);
}

View file

@ -0,0 +1,51 @@
/**
* @file command_zone_logic.h
* @ingroup GameLogicZones
* @brief Logic layer for the command zone, used for Commander format.
*/
#ifndef COCKATRICE_COMMAND_ZONE_LOGIC_H
#define COCKATRICE_COMMAND_ZONE_LOGIC_H
#include "card_zone_logic.h"
/**
* @class CommandZoneLogic
* @brief Logic layer for managing cards in the command zone.
*
* Handles data storage and card management for the command zone in Commander format.
* Supports ordered card insertion for drag-and-drop operations.
*
* @see CommandZone for the graphics layer
* @see CardZoneLogic
*/
class CommandZoneLogic : public CardZoneLogic
{
Q_OBJECT
public:
/**
* @brief Constructs a CommandZoneLogic instance.
* @param _player The player who owns this zone
* @param _name Zone name (ZoneNames::COMMAND)
* @param _hasCardAttr Whether cards in this zone have attributes
* @param _isShufflable Whether the zone can be shuffled
* @param _contentsKnown Whether the zone contents are public knowledge
* @param parent Parent QObject
*/
CommandZoneLogic(PlayerLogic *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
protected:
/**
* @brief Adds a card at position x (y ignored). Appends if x is -1 or out of range.
* @param card Card to add
* @param x Insertion index, or -1 to append
* @param y Unused
*/
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_COMMAND_ZONE_LOGIC_H