diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index bd99d08bf..649cd49cd 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -67,6 +67,7 @@ set(cockatrice_SOURCES src/game_graphics/board/card_item.cpp src/game/board/card_list.cpp src/game/board/card_state.cpp + src/game_graphics/board/commander_tax_counter.cpp src/game_graphics/board/counter_general.cpp src/game/board/counter_state.cpp src/game_graphics/board/translate_counter_name.cpp @@ -86,7 +87,8 @@ set(cockatrice_SOURCES src/game_graphics/log/message_log_widget.cpp src/game/phase.cpp src/game_graphics/phases_toolbar.cpp - src/game_graphics/player/menu/card_menu.cpp + src/game_graphics/player/menu/card_menu. + src/game_graphics/player/menu/command_zone_menu.cpp src/game_graphics/player/menu/custom_zone_menu.cpp src/game_graphics/player/menu/grave_menu.cpp src/game_graphics/player/menu/hand_menu.cpp @@ -110,6 +112,8 @@ set(cockatrice_SOURCES src/game_graphics/player/player_target.cpp src/game/replay.cpp src/game/zones/card_zone_logic.cpp + src/game/zones/command_zone.cpp + src/game/zones/command_zone_logic.cpp src/game/zones/hand_zone_logic.cpp src/game/zones/pile_zone_logic.cpp src/game/zones/stack_zone_logic.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 64416e5ee..baec2bb05 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -413,6 +413,7 @@ SettingsCache::SettingsCache() createGameAsSpectator = settings->value("game/creategameasspectator", false).toBool(); defaultStartingLifeTotal = settings->value("game/defaultstartinglifetotal", 20).toInt(); shareDecklistsOnLoad = settings->value("game/sharedecklistsonload", false).toBool(); + enableCommandZone = settings->value("game/enablecommandzone", false).toBool(); rememberGameSettings = settings->value("game/remembergamesettings", true).toBool(); // Local game settings use "localgameoptions/" prefix to keep them separate @@ -420,6 +421,7 @@ SettingsCache::SettingsCache() localGameRememberSettings = settings->value("localgameoptions/remembersettings", false).toBool(); localGameMaxPlayers = settings->value("localgameoptions/maxplayers", 1).toInt(); localGameStartingLifeTotal = settings->value("localgameoptions/startinglifetotal", 20).toInt(); + localGameEnableCommandZone = settings->value("localgameoptions/enablecommandzone", false).toBool(); clientID = settings->value("personal/clientid", CLIENT_INFO_NOT_SET).toString(); clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString(); @@ -1258,6 +1260,12 @@ void SettingsCache::setShareDecklistsOnLoad(const bool _shareDecklistsOnLoad) settings->setValue("game/sharedecklistsonload", shareDecklistsOnLoad); } +void SettingsCache::setEnableCommandZone(const bool _enableCommandZone) +{ + enableCommandZone = _enableCommandZone; + settings->setValue("game/enablecommandzone", enableCommandZone); +} + void SettingsCache::setCheckUpdatesOnStartup(QT_STATE_CHANGED_T value) { checkUpdatesOnStartup = static_cast(value); @@ -1318,6 +1326,12 @@ void SettingsCache::setLocalGameStartingLifeTotal(int value) settings->setValue("localgameoptions/startinglifetotal", value); } +void SettingsCache::setLocalGameEnableCommandZone(bool value) +{ + localGameEnableCommandZone = value; + settings->setValue("localgameoptions/enablecommandzone", value); +} + void SettingsCache::setNotifyAboutUpdate(QT_STATE_CHANGED_T _notifyaboutupdate) { notifyAboutUpdates = static_cast(_notifyaboutupdate); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index b1197e267..35cdd13da 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -330,6 +330,7 @@ private: bool createGameAsSpectator; int defaultStartingLifeTotal; bool shareDecklistsOnLoad; + bool enableCommandZone; int keepalive; int timeout; void translateLegacySettings(); @@ -342,6 +343,7 @@ private: bool localGameRememberSettings; int localGameMaxPlayers; int localGameStartingLifeTotal; + bool localGameEnableCommandZone; QList releaseChannels; bool isPortableBuild; @@ -889,6 +891,10 @@ public: { return shareDecklistsOnLoad; } + [[nodiscard]] bool getEnableCommandZone() const + { + return enableCommandZone; + } [[nodiscard]] bool getCreateGameAsSpectator() const { return createGameAsSpectator; @@ -909,6 +915,10 @@ public: { return localGameStartingLifeTotal; } + [[nodiscard]] bool getLocalGameEnableCommandZone() const + { + return localGameEnableCommandZone; + } [[nodiscard]] int getKeepAlive() const override { return keepalive; @@ -1138,10 +1148,12 @@ public slots: void setCreateGameAsSpectator(const bool _createGameAsSpectator); void setDefaultStartingLifeTotal(const int _defaultStartingLifeTotal); void setShareDecklistsOnLoad(const bool _shareDecklistsOnLoad); + void setEnableCommandZone(const bool _enableCommandZone); void setRememberGameSettings(const bool _rememberGameSettings); void setLocalGameRememberSettings(bool value); void setLocalGameMaxPlayers(int value); void setLocalGameStartingLifeTotal(int value); + void setLocalGameEnableCommandZone(bool value); void setCheckUpdatesOnStartup(QT_STATE_CHANGED_T value); void setStartupCardUpdateCheckPromptForUpdate(bool value); void setStartupCardUpdateCheckAlwaysUpdate(bool value); diff --git a/cockatrice/src/game/board/commander_tax_counter.cpp b/cockatrice/src/game/board/commander_tax_counter.cpp new file mode 100644 index 000000000..d0ae0ad5c --- /dev/null +++ b/cockatrice/src/game/board/commander_tax_counter.cpp @@ -0,0 +1,60 @@ +#include "commander_tax_counter.h" + +#include "counter_state.h" +#include "translate_counter_name.h" + +#include +#include +#include + +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(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)); +} diff --git a/cockatrice/src/game/board/commander_tax_counter.h b/cockatrice/src/game/board/commander_tax_counter.h new file mode 100644 index 000000000..220113303 --- /dev/null +++ b/cockatrice/src/game/board/commander_tax_counter.h @@ -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 diff --git a/cockatrice/src/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp index 6da18b662..b8377102f 100644 --- a/cockatrice/src/game/board/counter_state.cpp +++ b/cockatrice/src/game/board/counter_state.cpp @@ -2,15 +2,22 @@ #include -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); } \ No newline at end of file diff --git a/cockatrice/src/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h index 0f2f16b55..4a0f48203 100644 --- a/cockatrice/src/game/board/counter_state.h +++ b/cockatrice/src/game/board/counter_state.h @@ -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 diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index de909ca5e..3fc3e3ef7 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -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 @@ -24,9 +25,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -1526,9 +1529,9 @@ void PlayerActions::offsetCardCounter(QList selectedCards, int count int oldValue = card->getCounters().value(counterId, 0); int newValue = oldValue + offset; - // Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD]. + // Early exit optimization: server enforces [0, MAX_COUNTER_VALUE]. // Compare clamped value to allow recovery from invalid states. - int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD); + int clampedValue = qBound(0, newValue, MAX_COUNTER_VALUE); if (clampedValue != oldValue) { auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); @@ -1562,7 +1565,7 @@ void PlayerActions::actSetCardCounter(QList selectedCards, int count Expression exp(oldValue); double parsed = exp.parse(counterValue); // Clamp in double precision first to avoid UB, then cast - int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTERS_ON_CARD))); + int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTER_VALUE))); auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); @@ -1592,7 +1595,7 @@ void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) counterIterator.next(); int counterId = counterIterator.key(); int currentValue = counterIterator.value(); - if (currentValue >= MAX_COUNTERS_ON_CARD) { + if (currentValue >= MAX_COUNTER_VALUE) { continue; } @@ -1624,6 +1627,14 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone) void PlayerActions::playSelectedCards(QList selectedCards, const bool faceDown) { + playSelectedCardsImpl(faceDown, nullptr); +} + +void PlayerActions::playSelectedCardsImpl(bool faceDown, + const std::function &postPlayCallback) +{ + QList 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. @@ -1632,11 +1643,68 @@ void PlayerActions::playSelectedCards(QList 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 selectedCards) { playSelectedCards(selectedCards, false); @@ -1917,6 +1985,18 @@ void PlayerActions::cardMenuAction(QList 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) { diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 3f1960892..3c552f509 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -126,6 +127,14 @@ public slots: void actPlay(QList selectedCards); void actPlayFacedown(QList 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 selectedCards); void actMoveTopCardToPlay(); @@ -219,6 +228,8 @@ public slots: void cardMenuAction(QList selectedCards, CardMenuActionType type); private: + void sendIncCounter(int counterId, int delta); + PlayerLogic *player; int defaultNumberTopCards = 1; @@ -244,6 +255,14 @@ private: void playSelectedCards(QList 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 &postPlayCallback = nullptr); + void cmdSetTopCard(Command_MoveCard &cmd); void cmdSetBottomCard(Command_MoveCard &cmd); diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index bc48298f7..6900e2c16 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -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 #include #include +#include #include #include +#include #include 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; diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index cfd82933f..6416b0ed7 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -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); diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 485e2fc5c..35b197763 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -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 @@ -28,11 +32,12 @@ #include #include #include +#include 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()) { diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index a89cb6eed..4bb22f80c 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -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(zones.value(ZoneNames::HAND)); } + /** @brief Returns the command zone logic, or nullptr if not present. */ + CommandZoneLogic *getCommandZone() + { + return qobject_cast(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 counters; bool dialogSemaphore; + bool serverHasCommandZone; QList cardsToDelete; }; diff --git a/cockatrice/src/game/zones/card_zone_logic.cpp b/cockatrice/src/game/zones/card_zone_logic.cpp index 7e0585f4e..136f8bd72 100644 --- a/cockatrice/src/game/zones/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/card_zone_logic.cpp @@ -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: diff --git a/cockatrice/src/game/zones/command_zone.cpp b/cockatrice/src/game/zones/command_zone.cpp new file mode 100644 index 000000000..cbac7ce31 --- /dev/null +++ b/cockatrice/src/game/zones/command_zone.cpp @@ -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 +#include +#include +#include + +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(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 &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); + } +} diff --git a/cockatrice/src/game/zones/command_zone.h b/cockatrice/src/game/zones/command_zone.h new file mode 100644 index 000000000..4a6143890 --- /dev/null +++ b/cockatrice/src/game/zones/command_zone.h @@ -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 + +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 &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 diff --git a/cockatrice/src/game/zones/command_zone_logic.cpp b/cockatrice/src/game/zones/command_zone_logic.cpp new file mode 100644 index 000000000..3e97ece41 --- /dev/null +++ b/cockatrice/src/game/zones/command_zone_logic.cpp @@ -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); +} diff --git a/cockatrice/src/game/zones/command_zone_logic.h b/cockatrice/src/game/zones/command_zone_logic.h new file mode 100644 index 000000000..8085537ab --- /dev/null +++ b/cockatrice/src/game/zones/command_zone_logic.h @@ -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 diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp index 219dd456e..ac092803d 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -79,6 +79,19 @@ void AbstractCounter::delCounter() } } +void AbstractCounter::setValue(int _value) +{ + value = _value; + update(); +} + +void AbstractCounter::setActive(bool _active) +{ + active = _active; + setVisible(_active); + update(); +} + void AbstractCounter::retranslateUi() { if (aSet) { diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h index b319a722d..d65ec9825 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -1,6 +1,7 @@ /** * @file abstract_counter.h * @ingroup GameGraphicsPlayers + * @brief Abstract base for player counters displayed on the game board. */ //! \todo Document this file. @@ -61,6 +62,13 @@ public: ~AbstractCounter() override; void retranslateUi() override; + + /** + * @brief Sets the counter value and triggers a visual update. + * Virtual to allow subclass display customization (e.g., CommanderTaxCounter tooltip updates). + * Overflow protection is handled server-side, not in client counter classes. + */ + virtual void setValue(int _value); void setShortcutsActive() override; void setShortcutsInactive() override; void delCounter(); @@ -93,6 +101,25 @@ public: { return shownInCounterArea; } + + /** + * @brief Returns whether this counter is active (visible and interactable). + * Inactive counters are hidden and their menu actions should be disabled. + */ + [[nodiscard]] bool isActive() const + { + return active; + } + + /** + * @brief Sets the active state of this counter. + * When inactive, the counter is hidden via setVisible(false). + * @param _active True to show and enable the counter, false to hide it + */ + virtual void setActive(bool _active); + +private: + bool active = true; }; class AbstractCounterDialog : public QInputDialog diff --git a/cockatrice/src/game_graphics/board/translate_counter_name.cpp b/cockatrice/src/game_graphics/board/translate_counter_name.cpp index 4f1c97657..892eea426 100644 --- a/cockatrice/src/game_graphics/board/translate_counter_name.cpp +++ b/cockatrice/src/game_graphics/board/translate_counter_name.cpp @@ -8,4 +8,6 @@ const QMap TranslateCounterName::translated = { {"r", QT_TRANSLATE_NOOP("TranslateCounterName", "Red")}, {"g", QT_TRANSLATE_NOOP("TranslateCounterName", "Green")}, {"x", QT_TRANSLATE_NOOP("TranslateCounterName", "Colorless")}, - {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}}; + {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}, + {"commander_tax_counter", QT_TRANSLATE_NOOP("TranslateCounterName", "Commander Tax")}, + {"partner_tax_counter", QT_TRANSLATE_NOOP("TranslateCounterName", "Partner Tax")}}; diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp index ccd903b04..423a66e92 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -80,6 +81,8 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position fromStr = tr(" from sideboard"); } else if (zoneName == ZoneNames::STACK) { fromStr = tr(" from the stack"); + } else if (zoneName == ZoneNames::COMMAND) { + fromStr = tr(" from the command zone"); } else { fromStr = tr(" from custom zone '%1'").arg(zoneName); } @@ -344,6 +347,8 @@ void MessageLogWidget::logMoveCard(PlayerLogic *player, } else { finalStr = tr("%1 plays %2%3."); } + } else if (targetZoneName == ZoneNames::COMMAND) { + finalStr = tr("%1 moves %2%3 to the command zone."); } else { fourthArg = targetZoneName; if (card->getFaceDown()) { @@ -671,6 +676,20 @@ void MessageLogWidget::logSetCounter(PlayerLogic *player, QString counterName, i soundEngine->playSound("life_change"); } + if (counterName == CounterNames::CommanderTax || counterName == CounterNames::PartnerTax) { + QString playerName = sanitizeHtml(player->getPlayerInfo()->getName()); + QString valueStr = QString("%1").arg(value); + int delta = value - oldValue; + QString counterDisplayName = TranslateCounterName::getDisplayName(counterName); + QString taxLabel = QString("%1").arg(sanitizeHtml(counterDisplayName)); + if (value > oldValue) { + appendHtmlServerMessage(tr("%1 increases %2 to %3 (+%4).").arg(playerName, taxLabel, valueStr).arg(delta)); + } else { + appendHtmlServerMessage(tr("%1 decreases %2 to %3 (%4).").arg(playerName, taxLabel, valueStr).arg(delta)); + } + return; + } + QString counterDisplayName = TranslateCounterName::getDisplayName(counterName); appendHtmlServerMessage(tr("%1 sets counter %2 to %3 (%4%5).") .arg(sanitizeHtml(player->getPlayerInfo()->getName())) diff --git a/cockatrice/src/game_graphics/player/card_menu_action_type.h b/cockatrice/src/game_graphics/player/card_menu_action_type.h index 4cae22716..f0fc1bd37 100644 --- a/cockatrice/src/game_graphics/player/card_menu_action_type.h +++ b/cockatrice/src/game_graphics/player/card_menu_action_type.h @@ -22,7 +22,8 @@ enum CardMenuActionType cmMoveToHand, cmMoveToGraveyard, cmMoveToExile, - cmMoveToTable + cmMoveToTable, + cmMoveToCommandZone }; #endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp index aa94c3be7..cac772023 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp @@ -2,6 +2,7 @@ #include "../../../client/settings/card_counter_settings.h" #include "../../../interface/widgets/tabs/tab_game.h" +#include "../../board/abstract_counter.h" #include "../../board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" @@ -14,6 +15,7 @@ #include #include #include +#include #include /** @@ -92,6 +94,12 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho aSelectRow = new QAction(this); aSelectColumn = new QAction(this); + aPlayAndIncreaseTax = new QAction(this); + connect(aPlayAndIncreaseTax, &QAction::triggered, playerActions, &PlayerActions::actPlayAndIncreaseTax); + aPlayAndIncreasePartnerTax = new QAction(this); + connect(aPlayAndIncreasePartnerTax, &QAction::triggered, playerActions, + &PlayerActions::actPlayAndIncreasePartnerTax); + connect(aAttach, &QAction::triggered, actions, &PlayerActions::actAttach); connect(aDrawArrow, &QAction::triggered, actions, &PlayerActions::actDrawArrow); connect(aSelectAll, &QAction::triggered, actions, &PlayerActions::actSelectAll); @@ -157,6 +165,33 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho } else if (card->getZone()->getName() == ZoneNames::EXILE || card->getZone()->getName() == ZoneNames::GRAVE) { createGraveyardOrExileMenu(writeableCard); + } else if (card->getZone()->getName() == ZoneNames::COMMAND) { + if (writeableCard) { + addAction(aPlay); + + AbstractCounter *cmdTax = player->getCounterWidget(CounterIds::CommanderTax); + if (cmdTax && cmdTax->isActive()) { + addAction(aPlayAndIncreaseTax); + } + + AbstractCounter *partnerTax = player->getCounterWidget(CounterIds::PartnerTax); + if (partnerTax && partnerTax->isActive()) { + addAction(aPlayAndIncreasePartnerTax); + } + + // No reveal submenu - command zone is public + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); + } else { + addAction(aDrawArrow); + addSeparator(); + addAction(aClone); + } + addSeparator(); + addAction(aSelectAll); + addRelatedCardView(); + addRelatedCardActions(); } else { createHandOrCustomZoneMenu(writeableCard); } @@ -487,6 +522,8 @@ void CardMenu::retranslateUi() aPlay->setText(tr("&Play")); aHide->setText(tr("&Hide")); aPlayFacedown->setText(tr("Play &Face Down")); + aPlayAndIncreaseTax->setText(tr("Play and &Increase Commander Tax")); + aPlayAndIncreasePartnerTax->setText(tr("Play and Increase &Partner Tax")); aRevealToAll->setText(tr("&All players")); //: Turn sideways or back again aTap->setText(tr("&Tap / Untap")); diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.h b/cockatrice/src/game_graphics/player/menu/card_menu.h index d67ef3876..c4eba1c46 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.h +++ b/cockatrice/src/game_graphics/player/menu/card_menu.h @@ -32,6 +32,9 @@ public: QMenu *mCardCounters; QAction *aPlay, *aPlayFacedown; + QAction * + aPlayAndIncreaseTax; ///< Plays card and increments the primary commander tax counter (CounterIds::CommanderTax) + QAction *aPlayAndIncreasePartnerTax; QAction *aRevealToAll; QAction *aHide; QAction *aClone; diff --git a/cockatrice/src/game_graphics/player/menu/command_zone_menu.cpp b/cockatrice/src/game_graphics/player/menu/command_zone_menu.cpp new file mode 100644 index 000000000..f7d979b7d --- /dev/null +++ b/cockatrice/src/game_graphics/player/menu/command_zone_menu.cpp @@ -0,0 +1,184 @@ +#include "command_zone_menu.h" + +#include "../../../client/settings/cache_settings.h" +#include "../../board/abstract_counter.h" +#include "../../game_scene.h" +#include "../../zones/command_zone.h" +#include "../player_actions.h" +#include "../player_graphics_item.h" +#include "../player_logic.h" + +#include +#include + +CommandZoneMenu::CommandZoneMenu(PlayerLogic *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) +{ + viewZoneShortcutKey = QStringLiteral("Player/aViewCommandZone"); + incTaxShortcutKey = QStringLiteral("Player/aAddCommanderTax"); + decTaxShortcutKey = QStringLiteral("Player/aRemoveCommanderTax"); + incPartnerTaxShortcutKey = QStringLiteral("Player/aAddPartnerTax"); + decPartnerTaxShortcutKey = QStringLiteral("Player/aRemovePartnerTax"); + + aViewZone = new QAction(this); + connect(aViewZone, &QAction::triggered, this, + [this]() { player->getGameScene()->toggleZoneView(player, ZoneNames::COMMAND, -1); }); + + if (player->getPlayerInfo()->getLocalOrJudge()) { + addAction(aViewZone); + addSeparator(); + + PlayerActions *playerActions = player->getPlayerActions(); + + aIncreaseCommanderTax = new QAction(this); + connect(aIncreaseCommanderTax, &QAction::triggered, this, + [playerActions]() { playerActions->actModifyTaxCounter(CounterIds::CommanderTax, 1); }); + addAction(aIncreaseCommanderTax); + + aDecreaseCommanderTax = new QAction(this); + connect(aDecreaseCommanderTax, &QAction::triggered, this, + [playerActions]() { playerActions->actModifyTaxCounter(CounterIds::CommanderTax, -1); }); + addAction(aDecreaseCommanderTax); + + addSeparator(); + + aIncreasePartnerTax = new QAction(this); + connect(aIncreasePartnerTax, &QAction::triggered, this, + [playerActions]() { playerActions->actModifyTaxCounter(CounterIds::PartnerTax, 1); }); + addAction(aIncreasePartnerTax); + + aDecreasePartnerTax = new QAction(this); + connect(aDecreasePartnerTax, &QAction::triggered, this, + [playerActions]() { playerActions->actModifyTaxCounter(CounterIds::PartnerTax, -1); }); + addAction(aDecreasePartnerTax); + + addSeparator(); + + aToggleCommanderTaxCounter = new QAction(this); + connect(aToggleCommanderTaxCounter, &QAction::triggered, this, + [playerActions]() { playerActions->actToggleTaxCounter(CounterIds::CommanderTax); }); + addAction(aToggleCommanderTaxCounter); + + aTogglePartnerTaxCounter = new QAction(this); + connect(aTogglePartnerTaxCounter, &QAction::triggered, this, + [playerActions]() { playerActions->actToggleTaxCounter(CounterIds::PartnerTax); }); + addAction(aTogglePartnerTaxCounter); + + addSeparator(); + + aToggleMinimized = new QAction(this); + connect(aToggleMinimized, &QAction::triggered, this, &CommandZoneMenu::actToggleMinimized); + addAction(aToggleMinimized); + + connect(this, &QMenu::aboutToShow, this, &CommandZoneMenu::updateTaxCounterActionStates); + } + + retranslateUi(); +} + +void CommandZoneMenu::retranslateUi() +{ + setTitle(tr("Co&mmander")); + if (aViewZone) { + aViewZone->setText(tr("&View command zone")); + } + if (aIncreaseCommanderTax) { + aIncreaseCommanderTax->setText(tr("&Increase Commander Tax (+1)")); + } + if (aDecreaseCommanderTax) { + aDecreaseCommanderTax->setText(tr("&Decrease Commander Tax (-1)")); + } + if (aToggleCommanderTaxCounter) { + aToggleCommanderTaxCounter->setText(tr("&Remove Commander Tax")); + } + if (aIncreasePartnerTax) { + aIncreasePartnerTax->setText(tr("Increase &Partner Tax (+1)")); + } + if (aDecreasePartnerTax) { + aDecreasePartnerTax->setText(tr("Decrease P&artner Tax (-1)")); + } + if (aTogglePartnerTaxCounter) { + aTogglePartnerTaxCounter->setText(tr("&Add Partner Tax")); + } + if (aToggleMinimized) { + aToggleMinimized->setText(tr("&Minimize")); + } +} + +void CommandZoneMenu::actToggleMinimized() +{ + CommandZone *zone = player->getGraphicsItem()->getCommandZoneGraphicsItem(); + if (zone) { + zone->toggleMinimized(); + } +} + +void CommandZoneMenu::updateTaxCounterActionStates() +{ + AbstractCounter *cmdTax = player->getCounterWidget(CounterIds::CommanderTax); + bool cmdActive = cmdTax && cmdTax->isActive(); + + AbstractCounter *partnerTax = player->getCounterWidget(CounterIds::PartnerTax); + bool partnerActive = partnerTax && partnerTax->isActive(); + + if (aIncreaseCommanderTax) { + aIncreaseCommanderTax->setVisible(cmdActive); + } + if (aDecreaseCommanderTax) { + aDecreaseCommanderTax->setVisible(cmdActive); + } + if (aToggleCommanderTaxCounter) { + aToggleCommanderTaxCounter->setText(cmdActive ? tr("&Remove Commander Tax") : tr("&Add Commander Tax")); + aToggleCommanderTaxCounter->setVisible(!cmdActive || (cmdTax && cmdTax->getValue() == 0)); + } + + if (aIncreasePartnerTax) { + aIncreasePartnerTax->setVisible(partnerActive); + } + if (aDecreasePartnerTax) { + aDecreasePartnerTax->setVisible(partnerActive); + } + if (aTogglePartnerTaxCounter) { + aTogglePartnerTaxCounter->setText(partnerActive ? tr("R&emove Partner Tax") : tr("&Add Partner Tax")); + aTogglePartnerTaxCounter->setVisible(!partnerActive || (partnerTax && partnerTax->getValue() == 0)); + } +} + +void CommandZoneMenu::setShortcutsActive() +{ + ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); + + if (aViewZone) { + aViewZone->setShortcuts(shortcuts.getShortcut(viewZoneShortcutKey)); + } + if (aIncreaseCommanderTax) { + aIncreaseCommanderTax->setShortcuts(shortcuts.getShortcut(incTaxShortcutKey)); + } + if (aDecreaseCommanderTax) { + aDecreaseCommanderTax->setShortcuts(shortcuts.getShortcut(decTaxShortcutKey)); + } + if (aIncreasePartnerTax) { + aIncreasePartnerTax->setShortcuts(shortcuts.getShortcut(incPartnerTaxShortcutKey)); + } + if (aDecreasePartnerTax) { + aDecreasePartnerTax->setShortcuts(shortcuts.getShortcut(decPartnerTaxShortcutKey)); + } +} + +void CommandZoneMenu::setShortcutsInactive() +{ + if (aViewZone) { + aViewZone->setShortcut(QKeySequence()); + } + if (aIncreaseCommanderTax) { + aIncreaseCommanderTax->setShortcut(QKeySequence()); + } + if (aDecreaseCommanderTax) { + aDecreaseCommanderTax->setShortcut(QKeySequence()); + } + if (aIncreasePartnerTax) { + aIncreasePartnerTax->setShortcut(QKeySequence()); + } + if (aDecreasePartnerTax) { + aDecreasePartnerTax->setShortcut(QKeySequence()); + } +} diff --git a/cockatrice/src/game_graphics/player/menu/command_zone_menu.h b/cockatrice/src/game_graphics/player/menu/command_zone_menu.h new file mode 100644 index 000000000..2f36c6d7a --- /dev/null +++ b/cockatrice/src/game_graphics/player/menu/command_zone_menu.h @@ -0,0 +1,62 @@ +/** + * @file command_zone_menu.h + * @ingroup GameMenusZones + * @brief Context menu for command zone right-click actions. + */ + +#ifndef COCKATRICE_COMMAND_ZONE_MENU_H +#define COCKATRICE_COMMAND_ZONE_MENU_H + +#include "abstract_player_component.h" + +#include + +class PlayerLogic; + +/** + * @class CommandZoneMenu + * @brief Context menu for the command zone. + * + * Appears when right-clicking on the command zone. Provides actions for + * viewing zone contents, adjusting the commander tax counter, and + * toggling minimized state. + * + * @see PlayerMenu + * @see CommandZone + */ +class CommandZoneMenu : public QMenu, public AbstractPlayerComponent +{ + Q_OBJECT + +public: + explicit CommandZoneMenu(PlayerLogic *player, QMenu *playerMenu); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; + + QAction *aViewZone = nullptr; ///< Opens a zone viewer for the command zone + +private: + QAction *aIncreaseCommanderTax = nullptr; ///< Increments the primary commander tax counter + QAction *aDecreaseCommanderTax = nullptr; ///< Decrements the primary commander tax counter + QAction *aToggleCommanderTaxCounter = nullptr; ///< Toggles primary commander tax counter visibility + QAction *aIncreasePartnerTax = nullptr; ///< Increments the partner commander tax counter + QAction *aDecreasePartnerTax = nullptr; ///< Decrements the partner commander tax counter + QAction *aTogglePartnerTaxCounter = nullptr; ///< Toggles partner commander tax counter visibility + QAction *aToggleMinimized = nullptr; ///< Toggles command zone minimized state + +private slots: + void actToggleMinimized(); + +private: + void updateTaxCounterActionStates(); + PlayerLogic *player; + + QString viewZoneShortcutKey; + QString incTaxShortcutKey; + QString decTaxShortcutKey; + QString incPartnerTaxShortcutKey; + QString decPartnerTaxShortcutKey; +}; + +#endif // COCKATRICE_COMMAND_ZONE_MENU_H diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp index 5b7209a9f..2c345a81c 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp @@ -20,6 +20,8 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) aMoveToGraveyard->setData(cmMoveToGraveyard); aMoveToExile = new QAction(this); aMoveToExile->setData(cmMoveToExile); + aMoveToCommandZone = new QAction(this); + aMoveToCommandZone->setData(cmMoveToCommandZone); auto *actions = player->getLogic()->getPlayerActions(); @@ -49,6 +51,8 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) addAction(aMoveToGraveyard); addSeparator(); addAction(aMoveToExile); + addSeparator(); + addAction(aMoveToCommandZone); setShortcutsActive(); @@ -65,6 +69,7 @@ void MoveMenu::setShortcutsActive() aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand")); aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard")); aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile")); + aMoveToCommandZone->setShortcuts(shortcuts.getShortcut("Player/aMoveToCommandZone")); } void MoveMenu::retranslateUi() @@ -76,4 +81,5 @@ void MoveMenu::retranslateUi() aMoveToHand->setText(tr("&Hand")); aMoveToGraveyard->setText(tr("&Graveyard")); aMoveToExile->setText(tr("&Exile")); + aMoveToCommandZone->setText(tr("&Command Zone")); } diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.h b/cockatrice/src/game_graphics/player/menu/move_menu.h index 150bdbd3c..af9f9b856 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.h +++ b/cockatrice/src/game_graphics/player/menu/move_menu.h @@ -26,6 +26,7 @@ public: QAction *aMoveToTable = nullptr; QAction *aMoveToGraveyard = nullptr; QAction *aMoveToExile = nullptr; + QAction *aMoveToCommandZone = nullptr; }; #endif // COCKATRICE_MOVE_MENU_H diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp index 17b791222..6fab513b0 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp @@ -5,6 +5,7 @@ #include "../../../game_graphics/zones/table_zone.h" #include "../../../interface/widgets/tabs/tab_game.h" #include "../../board/card_item.h" +#include "../../zones/command_zone.h" #include "../player_graphics_item.h" #include "card_menu.h" #include "hand_menu.h" @@ -31,6 +32,16 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_ if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { sideboardMenu = addManagedMenu(player, playerMenu); + + commandZoneMenu = addManagedMenu(player, playerMenu); + auto updateCommandZoneMenuVisibility = [this](bool has) { + if (commandZoneMenu) { + commandZoneMenu->menuAction()->setVisible(has); + } + }; + connect(player, &PlayerLogic::commandZoneSupportChanged, this, updateCommandZoneMenuVisibility); + updateCommandZoneMenuVisibility(player->hasServerCommandZone()); + customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); @@ -39,6 +50,7 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_ utilityMenu = createManagedComponent(player, playerMenu); } else { sideboardMenu = nullptr; + commandZoneMenu = nullptr; customZonesMenu = nullptr; countersMenu = nullptr; utilityMenu = nullptr; @@ -66,6 +78,10 @@ void PlayerMenu::setMenusForGraphicItems() player->getHandZoneGraphicsItem()->setMenu(handMenu); player->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard); player->getSideboardZoneGraphicsItem()->setMenu(sideboardMenu); + + if (auto *commandZone = player->getCommandZoneGraphicsItem()) { + commandZone->setMenu(commandZoneMenu, commandZoneMenu->aViewZone); + } } } diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h index 62ba66df7..e77401f67 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.h +++ b/cockatrice/src/game_graphics/player/menu/player_menu.h @@ -8,6 +8,8 @@ #define COCKATRICE_PLAYER_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "../player_logic.h" +#include "command_zone_menu.h" #include "custom_zone_menu.h" #include "grave_menu.h" #include "hand_menu.h" @@ -88,6 +90,7 @@ private: RfgMenu *rfgMenu; UtilityMenu *utilityMenu; SayMenu *sayMenu; + CommandZoneMenu *commandZoneMenu; CustomZoneMenu *customZonesMenu; /** @brief Drives AbstractPlayerComponent lifecycle delegation. Counters are iterated separately via diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 07975ed5e..e02e663a7 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -3,6 +3,7 @@ #include "../../game/player/player_actions.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/abstract_card_item.h" +#include "../board/commander_tax_counter.h" #include "../board/counter_general.h" #include "../hand_counter.h" #include "../zones/hand_zone.h" @@ -13,6 +14,10 @@ #include "player_dialogs.h" #include +#include "../z_values.h" +#include "../zones/command_zone.h" + +#include PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) { @@ -118,6 +123,12 @@ void PlayerGraphicsItem::initializeZones() new HandZone(player->getHandZone(), static_cast(tableZoneGraphicsItem->boundingRect().height()), this); connect(player->getPlayerActions(), &PlayerActions::requestSortHand, handZoneGraphicsItem, &HandZone::sortHand); + // Command zone + commandZoneGraphicsItem = new CommandZone(player->getCommandZone(), ZoneSizes::COMMAND_ZONE_HEIGHT, this); + commandZoneGraphicsItem->setZValue(ZValues::COMMAND_ZONE); + commandZoneGraphicsItem->setVisible(false); + connect(commandZoneGraphicsItem, &CommandZone::minimizedChanged, this, &PlayerGraphicsItem::rearrangeZones); + connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter, &HandCounter::updateNumber); connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu); @@ -171,6 +182,13 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state) AbstractCounter *widget; if (state->getName() == "life") { widget = playerTarget->addCounter(state); + } else if (CounterNames::isTaxCounter(state->getName())) { + if (!commandZoneGraphicsItem) { + qWarning() << "Cannot create tax counter" << state->getName() << "- command zone not available"; + return; + } + widget = new CommanderTaxCounter(state, player, commandZoneGraphicsItem); + widget->setActive(state->isActive()); } else { widget = new GeneralCounter(state, player, true, this); } @@ -202,9 +220,16 @@ void PlayerGraphicsItem::onCounterRemoved(int counterId) void PlayerGraphicsItem::rearrangeCounters() { + if (commandZoneGraphicsItem) { + commandZoneGraphicsItem->rearrangeTaxCounters(); + } + qreal ySize = boundingRect().y() + 80; constexpr qreal padding = 5; for (auto *ctr : counterWidgets.values()) { + if (CounterNames::isTaxCounter(ctr->getName())) { + continue; + } if (!ctr->getShownInCounterArea()) { continue; } @@ -214,9 +239,33 @@ void PlayerGraphicsItem::rearrangeCounters() } } +QList PlayerGraphicsItem::getTaxCounterWidgets() const +{ + QList result; + for (AbstractCounter *ctr : counterWidgets.values()) { + if (CounterNames::isTaxCounter(ctr->getName())) { + result.append(ctr); + } + } + return result; +} + void PlayerGraphicsItem::rearrangeZones() { auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0); + + // Calculate stack height, accounting for command zone if visible + bool commandZoneVisible = commandZoneGraphicsItem && commandZoneGraphicsItem->isVisible(); + qreal tableHeight = tableZoneGraphicsItem->boundingRect().height(); + qreal stackHeight = tableHeight; + if (commandZoneVisible) { + stackHeight = tableHeight - totalCommandZoneHeight(); + if (stackHeight < CommandZone::MINIMUM_STACKING_HEIGHT) { + stackHeight = CommandZone::MINIMUM_STACKING_HEIGHT; + } + } + stackZoneGraphicsItem->setHeight(stackHeight); + if (SettingsCache::instance().getHorizontalHand()) { if (mirrored) { if (player->getHandZone()->contentsKnown()) { @@ -227,12 +276,12 @@ void PlayerGraphicsItem::rearrangeZones() handVisible = false; } - stackZoneGraphicsItem->setPos(base); + positionCommandAndStackZones(base); base += QPointF(stackZoneGraphicsItem->boundingRect().width(), 0); tableZoneGraphicsItem->setPos(base); } else { - stackZoneGraphicsItem->setPos(base); + positionCommandAndStackZones(base); tableZoneGraphicsItem->setPos(base.x() + stackZoneGraphicsItem->boundingRect().width(), 0); base += QPointF(0, tableZoneGraphicsItem->boundingRect().height()); @@ -252,7 +301,7 @@ void PlayerGraphicsItem::rearrangeZones() handZoneGraphicsItem->setPos(base); base += QPointF(handZoneGraphicsItem->boundingRect().width(), 0); - stackZoneGraphicsItem->setPos(base); + positionCommandAndStackZones(base); base += QPointF(stackZoneGraphicsItem->boundingRect().width(), 0); tableZoneGraphicsItem->setPos(base); @@ -281,3 +330,28 @@ void PlayerGraphicsItem::updateBoundingRect() emit sizeChanged(); } + +qreal PlayerGraphicsItem::totalCommandZoneHeight() const +{ + if (commandZoneGraphicsItem && commandZoneGraphicsItem->isVisible()) { + return commandZoneGraphicsItem->currentHeight(); + } + return 0; +} + +void PlayerGraphicsItem::positionCommandAndStackZones(const QPointF &base) +{ + bool commandZoneVisible = commandZoneGraphicsItem && commandZoneGraphicsItem->isVisible(); + if (commandZoneVisible) { + commandZoneGraphicsItem->setPos(base); + } + stackZoneGraphicsItem->setPos(base.x(), base.y() + (commandZoneVisible ? totalCommandZoneHeight() : 0)); +} + +void PlayerGraphicsItem::setCommandZoneVisible(bool visible) +{ + if (commandZoneGraphicsItem) { + commandZoneGraphicsItem->setVisible(visible); + } + rearrangeZones(); +} diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h index 0dcc959bd..39761e825 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -12,6 +12,7 @@ #include +class CommandZone; class HandZone; class PileZone; class PlayerDialogs; @@ -107,6 +108,18 @@ public: { return handZoneGraphicsItem; } + /** @brief Returns the command zone graphics item. */ + [[nodiscard]] CommandZone *getCommandZoneGraphicsItem() const + { + return commandZoneGraphicsItem; + } + /** @brief Returns the counter widget for the given counter ID, or nullptr if not found. */ + [[nodiscard]] AbstractCounter *getCounterWidget(int counterId) const + { + return counterWidgets.value(counterId, nullptr); + } + /** @brief Returns all tax counter widgets (commander tax and partner tax). */ + [[nodiscard]] QList getTaxCounterWidgets() const; public slots: void onPlayerActiveChanged(bool _active); @@ -114,6 +127,8 @@ public slots: void onCounterRemoved(int counterId); void rearrangeCounters(); void retranslateUi(); + /** @brief Shows or hides the command zone and rearranges dependent zones. */ + void setCommandZoneVisible(bool visible); signals: void sizeChanged(); @@ -135,10 +150,15 @@ private: TableZone *tableZoneGraphicsItem; StackZone *stackZoneGraphicsItem; HandZone *handZoneGraphicsItem; + CommandZone *commandZoneGraphicsItem; QRectF bRect; bool mirrored; bool handVisible = false; + /** @brief Returns the command zone's display height, or 0 if hidden. */ + [[nodiscard]] qreal totalCommandZoneHeight() const; + /** @brief Positions the command and stack zones vertically starting from base, updating base.y. */ + void positionCommandAndStackZones(const QPointF &base); private slots: void updateBoundingRect(); void rearrangeZones(); diff --git a/cockatrice/src/game_graphics/z_values.h b/cockatrice/src/game_graphics/z_values.h index c6e7f2c8a..7b30054a9 100644 --- a/cockatrice/src/game_graphics/z_values.h +++ b/cockatrice/src/game_graphics/z_values.h @@ -29,11 +29,16 @@ namespace ZValues { +/** @brief Command zone sits at standard zone level */ +constexpr qreal COMMAND_ZONE = 1.0; + // Expose base for callers that need it constexpr qreal OVERLAY_BASE = ZValueLayerManager::OVERLAY_BASE; // Overlay layer Z-values for items that should appear above normal cards constexpr qreal HOVERED_CARD = ZValueLayerManager::overlayZValue(1.0); +/** @brief Commander tax counter overlay */ +constexpr qreal TAX_COUNTERS = ZValueLayerManager::overlayZValue(2.0); constexpr qreal ARROWS = ZValueLayerManager::overlayZValue(3.0); constexpr qreal ZONE_VIEW_WIDGET = ZValueLayerManager::overlayZValue(4.0); constexpr qreal DRAG_ITEM = ZValueLayerManager::overlayZValue(5.0); diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index 46ff099ab..c3015496c 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -32,6 +32,10 @@ QRectF StackZone::boundingRect() const void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId()); + + QPointF scenePos = mapToScene(QPointF(0, 0)); + painter->setBrushOrigin(-scenePos); + painter->fillRect(boundingRect(), brush); } diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 086845fe6..5bd8cee71 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -25,10 +25,12 @@ #define PLAYERZONE_BG_NAME "playerzone" #define STACKZONE_BG_NAME "stackzone" #define TABLEZONE_BG_NAME "tablezone" +#define COMMANDZONE_BG_NAME "commandzone" static const QColor HANDZONE_BG_DEFAULT = QColor(80, 100, 50); static const QColor TABLEZONE_BG_DEFAULT = QColor(70, 50, 100); static const QColor PLAYERZONE_BG_DEFAULT = QColor(200, 200, 200); static const QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43); +static const QColor COMMANDZONE_BG_DEFAULT = QColor(50, 60, 80); static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"}; struct PaletteColorInfo @@ -271,6 +273,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, const PaletteConfig &palCfg, const QString &activeScheme) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0)) + Q_UNUSED(activeScheme) +#endif QString styleName = themeCfg.styleName; if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (themeName == FUSION_THEME_NAME) { @@ -370,6 +375,8 @@ void ThemeManager::themeChangedSlot() brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT); brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT); + + brushes[Role::Command] = loadBrush(COMMANDZONE_BG_NAME, COMMANDZONE_BG_DEFAULT); for (auto &brushCache : brushesCache) { brushCache.clear(); } @@ -394,6 +401,9 @@ static QString roleBgName(ThemeManager::Role role) case ThemeManager::Table: return TABLEZONE_BG_NAME; + case ThemeManager::Command: + return COMMANDZONE_BG_NAME; + default: Q_ASSERT(false); } diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index b9e764d08..02a728233 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -38,7 +38,8 @@ public: Stack, Table, Player, - MaxRole = Player, + Command, + MaxRole = Command, }; private: diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 30364f242..fa96c55ea 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -102,6 +102,7 @@ void DlgCreateGame::sharedCtor() startingLifeTotalLabel->setBuddy(startingLifeTotalEdit); shareDecklistsOnLoadCheckBox = new QCheckBox(tr("Open decklists in lobby")); + enableCommandZoneCheckBox = new QCheckBox(tr("Enable command zone")); createGameAsJudgeCheckBox = new QCheckBox(tr("Create game as judge")); @@ -109,6 +110,7 @@ void DlgCreateGame::sharedCtor() gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0); gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1); gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0); + gameSetupOptionsLayout->addWidget(enableCommandZoneCheckBox, 1, 1); if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) { gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0); } else { @@ -171,6 +173,7 @@ DlgCreateGame::DlgCreateGame(TabRoom *_room, const QMap &_gameType createGameAsSpectatorCheckBox->setChecked(SettingsCache::instance().getCreateGameAsSpectator()); startingLifeTotalEdit->setValue(SettingsCache::instance().getDefaultStartingLifeTotal()); shareDecklistsOnLoadCheckBox->setChecked(SettingsCache::instance().getShareDecklistsOnLoad()); + enableCommandZoneCheckBox->setChecked(SettingsCache::instance().getEnableCommandZone()); if (!rememberGameSettings->isChecked()) { actReset(); @@ -204,6 +207,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMapsetEnabled(false); startingLifeTotalEdit->setEnabled(false); shareDecklistsOnLoadCheckBox->setEnabled(false); + enableCommandZoneCheckBox->setEnabled(false); descriptionEdit->setText(QString::fromStdString(gameInfo.description())); maxPlayersEdit->setValue(gameInfo.max_players()); @@ -250,6 +254,7 @@ void DlgCreateGame::actReset() startingLifeTotalEdit->setValue(20); shareDecklistsOnLoadCheckBox->setChecked(false); + enableCommandZoneCheckBox->setChecked(false); createGameAsJudgeCheckBox->setChecked(false); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); @@ -280,6 +285,7 @@ void DlgCreateGame::actOK() cmd.set_join_as_spectator(createGameAsSpectatorCheckBox->isChecked()); cmd.set_starting_life_total(startingLifeTotalEdit->value()); cmd.set_share_decklists_on_load(shareDecklistsOnLoadCheckBox->isChecked()); + cmd.set_enable_command_zone(enableCommandZoneCheckBox->isChecked()); auto _gameTypes = QString(); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); @@ -304,6 +310,7 @@ void DlgCreateGame::actOK() SettingsCache::instance().setCreateGameAsSpectator(createGameAsSpectatorCheckBox->isChecked()); SettingsCache::instance().setDefaultStartingLifeTotal(startingLifeTotalEdit->value()); SettingsCache::instance().setShareDecklistsOnLoad(shareDecklistsOnLoadCheckBox->isChecked()); + SettingsCache::instance().setEnableCommandZone(enableCommandZoneCheckBox->isChecked()); SettingsCache::instance().setGameTypes(_gameTypes); } PendingCommand *pend = room->prepareRoomCommand(cmd); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h index 61925286d..4bb5eeb64 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h @@ -48,6 +48,7 @@ private: QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox, *spectatorsSeeEverythingCheckBox, *createGameAsJudgeCheckBox, *createGameAsSpectatorCheckBox; QCheckBox *shareDecklistsOnLoadCheckBox; + QCheckBox *enableCommandZoneCheckBox; QDialogButtonBox *buttonBox; QPushButton *clearButton; QCheckBox *rememberGameSettings; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp index d2d291556..cbd7383b3 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp @@ -33,10 +33,13 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) startingLifeTotalEdit->setValue(20); startingLifeTotalLabel->setBuddy(startingLifeTotalEdit); + enableCommandZoneCheckBox = new QCheckBox(tr("Enable command zone"), this); + auto *gameSetupGrid = new QGridLayout; gameSetupGrid->setContentsMargins(5, 5, 5, 5); gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0); gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1); + gameSetupGrid->addWidget(enableCommandZoneCheckBox, 1, 0, 1, 2); gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this); gameSetupOptionsGroupBox->setLayout(gameSetupGrid); @@ -57,6 +60,7 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) if (rememberSettingsCheckBox->isChecked()) { numberPlayersEdit->setValue(SettingsCache::instance().getLocalGameMaxPlayers()); startingLifeTotalEdit->setValue(SettingsCache::instance().getLocalGameStartingLifeTotal()); + enableCommandZoneCheckBox->setChecked(SettingsCache::instance().getLocalGameEnableCommandZone()); } setWindowTitle(tr("Local game options")); @@ -71,6 +75,7 @@ void DlgLocalGameOptions::actOK() if (rememberSettingsCheckBox->isChecked()) { SettingsCache::instance().setLocalGameMaxPlayers(numberPlayersEdit->value()); SettingsCache::instance().setLocalGameStartingLifeTotal(startingLifeTotalEdit->value()); + SettingsCache::instance().setLocalGameEnableCommandZone(enableCommandZoneCheckBox->isChecked()); } accept(); @@ -81,5 +86,6 @@ LocalGameOptions DlgLocalGameOptions::getOptions() const return LocalGameOptions{ .numberPlayers = numberPlayersEdit->value(), .startingLifeTotal = startingLifeTotalEdit->value(), + .enableCommandZone = enableCommandZoneCheckBox->isChecked(), }; } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h index 4307581a4..d52257ec6 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h @@ -16,6 +16,7 @@ struct LocalGameOptions { int numberPlayers = 1; int startingLifeTotal = 20; + bool enableCommandZone = false; }; class QCheckBox; @@ -45,6 +46,7 @@ private: QLabel *startingLifeTotalLabel; QSpinBox *startingLifeTotalEdit; + QCheckBox *enableCommandZoneCheckBox; QCheckBox *rememberSettingsCheckBox; QDialogButtonBox *buttonBox; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index a81161e83..a6fc3f151 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -690,7 +690,7 @@ void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId) auto *deckView = new TabbedDeckViewContainer(playerId, this); connect(deckView->playerDeckView, &DeckViewContainer::newCardAdded, this, &TabGame::newCardAdded); deckViewContainers.insert(playerId, deckView); - deckViewContainerLayout->addWidget(deckView); + deckViewContainerLayout->insertWidget(0, deckView, 1); // auto load deck for player if that debug setting is enabled QString deckPath = SettingsCache::instance().debug().getDeckPathForPlayer(newPlayer->getPlayerInfo()->getName()); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 69d3260bc..c4ad79085 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -171,6 +171,7 @@ void MainWindow::startLocalGame(const LocalGameOptions &options) Command_CreateGame createCommand; createCommand.set_max_players(static_cast(options.numberPlayers)); createCommand.set_starting_life_total(options.startingLifeTotal); + createCommand.set_enable_command_zone(options.enableCommandZone); mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0)); } diff --git a/cockatrice/themes/Fabric/zones/commandzone.png b/cockatrice/themes/Fabric/zones/commandzone.png new file mode 100644 index 000000000..e71f466e8 Binary files /dev/null and b/cockatrice/themes/Fabric/zones/commandzone.png differ diff --git a/cockatrice/themes/Leather/zones/commandzone.png b/cockatrice/themes/Leather/zones/commandzone.png new file mode 100644 index 000000000..8159dfaa0 Binary files /dev/null and b/cockatrice/themes/Leather/zones/commandzone.png differ diff --git a/cockatrice/themes/Plasma/zones/commandzone.png b/cockatrice/themes/Plasma/zones/commandzone.png new file mode 100644 index 000000000..115a0d613 Binary files /dev/null and b/cockatrice/themes/Plasma/zones/commandzone.png differ diff --git a/cockatrice/themes/VelvetMarble/zones/commandzone.png b/cockatrice/themes/VelvetMarble/zones/commandzone.png new file mode 100644 index 000000000..59490d3df Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/commandzone.png differ diff --git a/doc/doxygen/groups/doc_groups.dox b/doc/doxygen/groups/doc_groups.dox index ec5158c69..d69cdc52a 100644 --- a/doc/doxygen/groups/doc_groups.dox +++ b/doc/doxygen/groups/doc_groups.dox @@ -476,8 +476,7 @@ * @ingroup GameMenus * @brief Menus for interacting with zones. * - * Provides contextual options for a CardZone such as the hand, - * library, graveyard, and battlefield. + * Provides contextual options for card zones. */ /** @@ -512,8 +511,7 @@ * @ingroup GameGraphics * @brief Graphical representations of zones. * - * Provides layout, visuals, and animations for a CardZone like the hand, - * library, battlefield, and graveyard. + * Provides layout, visuals, and animations for card zones. */ /** @@ -548,8 +546,7 @@ * @ingroup GameLogic * @brief Logical handling of CardZones during a Game. * - * Defines the rules and behaviors of zones such as the hand, - * battlefield, library, and graveyard. + * Defines the rules and behaviors of card zones. */ /** diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index 493b8e966..59f64705a 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -342,6 +343,13 @@ Response::ResponseCode Server_AbstractParticipant::cmdDelCounter(const Command_D return Response::RespFunctionNotAllowed; } +Response::ResponseCode Server_AbstractParticipant::cmdSetCounterActive(const Command_SetCounterActive & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) @@ -525,6 +533,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo case GameCommand::REVERSE_TURN: return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges); break; + case GameCommand::SET_COUNTER_ACTIVE: + return cmdSetCounterActive(command.GetExtension(Command_SetCounterActive::ext), rc, ges); + break; default: return Response::RespInvalidCommand; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index a24fa5799..14de20ed0 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -41,6 +41,7 @@ class Command_Judge; class Command_IncCounter; class Command_CreateCounter; class Command_SetCounter; +class Command_SetCounterActive; class Command_DelCounter; class Command_NextTurn; class Command_SetActivePhase; @@ -161,6 +162,8 @@ public: virtual Response::ResponseCode cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode + cmdSetCounterActive(const Command_SetCounterActive &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp index b858314c0..8781a5788 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -114,8 +114,8 @@ QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) { - // Clamp to valid card counter range [0, MAX_COUNTERS_ON_CARD] - value = qBound(0, value, MAX_COUNTERS_ON_CARD); + // Clamp to valid card counter range [0, MAX_COUNTER_VALUE] + value = qBound(0, value, MAX_COUNTER_VALUE); const int oldValue = counters.value(_id, 0); if (value == oldValue) { @@ -140,9 +140,9 @@ bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounte { const int oldValue = counters.value(counterId, 0); const auto result = static_cast(oldValue) + static_cast(delta); - // Clamp to [0, MAX_COUNTERS_ON_CARD] for card counters + // Clamp to [0, MAX_COUNTER_VALUE] for card counters const int newValue = - static_cast(qBound(static_cast(0), result, static_cast(MAX_COUNTERS_ON_CARD))); + static_cast(qBound(static_cast(0), result, static_cast(MAX_COUNTER_VALUE))); if (newValue == oldValue) { return false; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h index 3d7e649b9..a2698ad61 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h @@ -156,7 +156,7 @@ public: /** * @brief Sets a card counter to an exact value with clamping. * @param _id The counter ID. - * @param value The desired value (clamped to [0, MAX_COUNTERS_ON_CARD]; 0 removes the counter). + * @param value The desired value (clamped to [0, MAX_COUNTER_VALUE]; 0 removes the counter). * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. */ @@ -168,7 +168,7 @@ public: * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. * @note If counter does not exist, starts from 0. Counter is removed if result is 0. - * @note Clamps result to [0, MAX_COUNTERS_ON_CARD]. + * @note Clamps result to [0, MAX_COUNTER_VALUE]. */ [[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr); void setTapped(bool _tapped) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp index e65205cbb..01a3a910f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -1,24 +1,19 @@ #include "server_counter.h" #include -#include -Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) - : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) +Server_Counter::Server_Counter(int _id, + const QString &_name, + const color &_counterColor, + int _radius, + int _count, + int _minValue, + int _maxValue) + : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count), minValue(_minValue), + maxValue(_maxValue) { } -//! \todo Extract overflow-safe arithmetic into shared helper. -//! Duplicated in Server_Card::incrementCounter() - keep in sync if modified. -bool Server_Counter::incrementCount(int delta) -{ - const int oldCount = count; - const auto result = static_cast(count) + static_cast(delta); - count = static_cast(qBound(static_cast(std::numeric_limits::min()), result, - static_cast(std::numeric_limits::max()))); - return count != oldCount; -} - void Server_Counter::getInfo(ServerInfo_Counter *info) { info->set_id(id); @@ -26,4 +21,5 @@ void Server_Counter::getInfo(ServerInfo_Counter *info) info->mutable_counter_color()->CopyFrom(counterColor); info->set_radius(radius); info->set_count(count); + info->set_active(active); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index 8226e663f..490a5c725 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -22,18 +22,18 @@ #include #include +#include class ServerInfo_Counter; /** * @class Server_Counter - * @brief Represents a player counter with overflow-safe increment arithmetic. + * @brief Represents a player counter with overflow-safe increment arithmetic and optional bounds. * * All value modifications return whether the value actually changed, * enabling callers to skip unnecessary network events. * - * @note Direct assignment via setCount() does not clamp; only - * incrementCount() enforces int boundary saturation. + * @note Values are clamped to [minValue, maxValue] on both setCount() and incrementCount(). * @note Unlike card counters, player counters are never auto-removed * when they reach zero - they persist with value 0. */ @@ -45,9 +45,30 @@ protected: color counterColor; int radius; int count; + int minValue; ///< Minimum allowed value (default: INT_MIN, i.e. unbounded) + int maxValue; ///< Maximum allowed value (default: INT_MAX, i.e. unbounded) + bool active = true; ///< Whether this counter is visible/active (default: true) + + static constexpr int DEFAULT_MAX_VALUE = std::numeric_limits::max(); public: - Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count = 0); + /** + * @brief Constructs a counter. + * @param _id Unique counter identifier + * @param _name Display name + * @param _counterColor Counter color + * @param _radius Display radius + * @param _count Initial value (default 0) + * @param _minValue Minimum allowed value (default INT_MIN) + * @param _maxValue Maximum allowed value (default INT_MAX) + */ + Server_Counter(int _id, + const QString &_name, + const color &_counterColor, + int _radius, + int _count = 0, + int _minValue = std::numeric_limits::min(), + int _maxValue = DEFAULT_MAX_VALUE); ~Server_Counter() { } @@ -71,18 +92,31 @@ public: { return count; } - + bool isActive() const + { + return active; + } /** - * @brief Sets the counter to an exact value. - * @param _count The new value (assigned directly without clamping). - * @return true if the value changed, false otherwise. - * @warning This performs raw assignment. For overflow-safe incrementing, - * use incrementCount(). + * @brief Sets the active (visible) state of this counter. + * @param _active True to show the counter, false to hide it + * @return true if the state changed + */ + [[nodiscard]] bool setActive(bool _active) + { + bool oldActive = active; + active = _active; + return active != oldActive; + } + /** + * @brief Sets the counter value, clamping to [minValue, maxValue]. + * @param _count The desired new value + * @return true if the clamped value differs from the previous value + * @note For increment operations, prefer incrementCount() which handles overflow safely. */ [[nodiscard]] bool setCount(int _count) { - const int oldCount = count; - count = _count; + int oldCount = count; + count = qBound(minValue, _count, maxValue); return count != oldCount; } @@ -90,9 +124,17 @@ public: * @brief Increments the counter by delta with overflow-safe arithmetic. * @param delta The amount to add (may be negative for decrement). * @return true if the value changed, false otherwise. - * @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow. + * @note Clamps result to [minValue, maxValue] to prevent overflow. */ - [[nodiscard]] bool incrementCount(int delta); + [[nodiscard]] bool incrementCount(int delta) + { + const auto result = static_cast(count) + static_cast(delta); + const int clamped = + static_cast(qBound(static_cast(minValue), result, static_cast(maxValue))); + int oldCount = count; + count = clamped; + return count != oldCount; + } /** * @brief Populates info with this counter's current state for network serialization. diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 4761199e5..f74458300 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -67,6 +67,7 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, bool _spectatorsSeeEverything, int _startingLifeTotal, bool _shareDecklistsOnLoad, + bool _enableCommandZone, Server_Room *_room) : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)), gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers), @@ -74,9 +75,9 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed), spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk), spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), - shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), - firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), - gameMutex() + shareDecklistsOnLoad(_shareDecklistsOnLoad), enableCommandZone(_enableCommandZone), inactivityCounter(0), + startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false), + startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex() { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index e0e7896b7..848d22b7a 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -69,6 +69,7 @@ private: bool spectatorsSeeEverything; int startingLifeTotal; bool shareDecklistsOnLoad; + bool enableCommandZone; int inactivityCounter; int startTimeOfThisGame, secondsElapsed; bool firstGameStarted; @@ -106,6 +107,7 @@ public: bool _spectatorsSeeEverything, int _startingLifeTotal, bool _shareDecklistsOnLoad, + bool _enableCommandZone, Server_Room *parent); ~Server_Game() override; Server_Room *getRoom() const @@ -173,6 +175,10 @@ public: { return shareDecklistsOnLoad; } + bool getEnableCommandZone() const + { + return enableCommandZone; + } Response::ResponseCode checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge); bool containsUser(const QString &userName) const; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index 56e3f9f8e..017b7e79b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +49,7 @@ #include #include #include +#include #include #include @@ -101,6 +104,17 @@ void Server_Player::setupZones() addCounter(new Server_Counter(6, "x", makeColor(255, 255, 255), 20, 0)); addCounter(new Server_Counter(7, "storm", makeColor(255, 150, 30), 20, 0)); + // Command zone for Commander format + if (game->getEnableCommandZone()) { + addZone(new Server_CardZone(this, ZoneNames::COMMAND, false, ServerInfo_Zone::PublicZone)); + addCounter(new Server_Counter(CounterIds::CommanderTax, CounterNames::CommanderTax, makeColor(128, 128, 128), + 20, 0, 0, MAX_COUNTER_VALUE)); + auto *partnerTax = new Server_Counter(CounterIds::PartnerTax, CounterNames::PartnerTax, + makeColor(128, 128, 128), 20, 0, 0, MAX_COUNTER_VALUE); + (void)partnerTax->setActive(false); + addCounter(partnerTax); + } + // ------------------------------------------------------------------ // Assign card ids and create deck from deck list @@ -426,6 +440,12 @@ Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer & return retVal; } +bool Server_Player::isCommandZoneCounterBlocked(int counterId) const +{ + return (counterId == CounterIds::CommanderTax || counterId == CounterIds::PartnerTax) && + !game->getEnableCommandZone(); +} + Response::ResponseCode Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { @@ -437,6 +457,11 @@ Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & } const int counterId = cmd.counter_id(); + + if (isCommandZoneCounterBlocked(counterId)) { + return Response::RespContextError; + } + Server_Counter *c = counters.value(counterId, nullptr); if (!c) { return Response::RespNameNotFound; @@ -490,6 +515,11 @@ Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer & } const int counterId = cmd.counter_id(); + + if (isCommandZoneCounterBlocked(counterId)) { + return Response::RespContextError; + } + Server_Counter *c = counters.value(counterId, nullptr); if (!c) { return Response::RespNameNotFound; @@ -517,6 +547,11 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & } const int counterId = cmd.counter_id(); + + if (isCommandZoneCounterBlocked(counterId)) { + return Response::RespContextError; + } + Server_Counter *counter = counters.value(counterId, nullptr); if (!counter) { return Response::RespNameNotFound; @@ -531,6 +566,35 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & return Response::RespOk; } +Response::ResponseCode Server_Player::cmdSetCounterActive(const Command_SetCounterActive &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + const int counterId = cmd.counter_id(); + Server_Counter *c = counters.value(counterId, nullptr); + if (!c) { + return Response::RespNameNotFound; + } + + bool didChange = c->setActive(cmd.active()); + + if (didChange) { + Event_SetCounterActive event; + event.set_counter_id(c->getId()); + event.set_active(c->isActive()); + ges.enqueueGameEvent(event, playerId); + } + + return Response::RespOk; +} + Response::ResponseCode Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h index 5925ed3c2..eb4559631 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h @@ -9,6 +9,7 @@ class Server_Player : public Server_AbstractPlayer private: QMap counters; QList lastDrawList; + bool isCommandZoneCounterBlocked(int counterId) const; public: Server_Player(Server_Game *_game, @@ -57,6 +58,8 @@ public: Response::ResponseCode cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; Response::ResponseCode + cmdSetCounterActive(const Command_SetCounterActive &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) override; Response::ResponseCode cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 27ebaf228..f7af9a113 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -885,10 +885,11 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room // When server doesn't permit registered users to exist, do not honor only-reg setting bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers()); - auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), - cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, - cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), - cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room); + auto *game = + new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), + cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, cmd.spectators_allowed(), + cmd.spectators_need_password(), cmd.spectators_can_talk(), cmd.spectators_see_everything(), + startingLifeTotal, shareDecklistsOnLoad, cmd.enable_command_zone(), room); game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index b4c7b6ac8..8769086a1 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -46,6 +46,7 @@ set(PROTO_FILES command_set_card_attr.proto command_set_card_counter.proto command_set_counter.proto + command_set_counter_active.proto command_set_sideboard_lock.proto command_set_sideboard_plan.proto command_shuffle.proto @@ -106,6 +107,7 @@ set(PROTO_FILES event_set_card_attr.proto event_set_card_counter.proto event_set_counter.proto + event_set_counter_active.proto event_shuffle.proto event_user_joined.proto event_user_left.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter_active.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter_active.proto new file mode 100644 index 000000000..232fd69d5 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter_active.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "game_commands.proto"; +message Command_SetCounterActive { + extend GameCommand { + optional Command_SetCounterActive ext = 1035; + } + optional sint32 counter_id = 1 [default = -1]; + optional bool active = 2 [default = true]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter_active.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter_active.proto new file mode 100644 index 000000000..9e674dfe3 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter_active.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "game_event.proto"; + +message Event_SetCounterActive { + extend GameEvent { + optional Event_SetCounterActive ext = 2023; + } + optional sint32 counter_id = 1; + optional bool active = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index 796f4fc68..78108e466 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -38,6 +38,7 @@ message GameCommand { UNCONCEDE = 1032; JUDGE = 1033; REVERSE_TURN = 1034; + SET_COUNTER_ACTIVE = 1035; } extensions 100 to max; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto index 7d3147701..3b829ebc6 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto @@ -34,6 +34,7 @@ message GameEvent { CHANGE_ZONE_PROPERTIES = 2020; REVERSE_TURN = 2021; GAME_LOG_NOTICE = 2022; + SET_COUNTER_ACTIVE = 2023; } optional sint32 player_id = 1 [default = -1]; extensions 100 to max; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto index a8c90ec6c..0ba36fe85 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -69,6 +69,9 @@ message Command_CreateGame { // share decklists with all players when selected optional bool share_decklists_on_load = 14; + + // enable command zone for Commander format + optional bool enable_command_zone = 15; } message Command_JoinGame { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto index 849e3b4e9..af56efe13 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto @@ -7,4 +7,5 @@ message ServerInfo_Counter { optional color counter_color = 3; optional sint32 radius = 4; optional sint32 count = 5; + optional bool active = 6 [default = true]; } diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index c0c7d8cc9..910077147 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -15,6 +15,7 @@ set(UTILITY_HEADERS libcockatrice/utility/levenshtein.h libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h + libcockatrice/utility/counter_ids.h libcockatrice/utility/trice_limits.h libcockatrice/utility/zone_names.h ) diff --git a/libcockatrice_utility/libcockatrice/utility/counter_ids.h b/libcockatrice_utility/libcockatrice/utility/counter_ids.h new file mode 100644 index 000000000..6745f4487 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/counter_ids.h @@ -0,0 +1,46 @@ +/** + * @file counter_ids.h + * @ingroup GameLogic + * @brief Shared counter IDs and names for system counters (e.g. commander tax). + */ + +#ifndef COCKATRICE_COUNTER_IDS_H +#define COCKATRICE_COUNTER_IDS_H + +#include + +/** + * Shared counter IDs used by both client and server. + * These must match between server_player.cpp and player_event_handler.cpp. + * + * Reserved counter IDs for system counters: + * IDs 0-7: Standard player counters (life, mana colors, storm) + * IDs 8-9: Commander tax counters + * IDs 10+: Available for user-created counters + * + * The server's newCounterId() starts from the highest existing ID + 1, + * so these reserved IDs won't conflict as long as they're created first + * during setupZones(). See server_player.cpp::setupZones() for the + * authoritative list of reserved IDs. + * + * To find all files referencing these IDs, grep for CounterIds::CommanderTax + * and CounterIds::PartnerTax across the codebase. + */ +namespace CounterIds +{ +constexpr int CommanderTax = 8; +constexpr int PartnerTax = 9; +} // namespace CounterIds + +namespace CounterNames +{ +constexpr const char *CommanderTax = "commander_tax_counter"; +constexpr const char *PartnerTax = "partner_tax_counter"; + +inline bool isTaxCounter(const QString &name) +{ + return name == CommanderTax || name == PartnerTax; +} +} // namespace CounterNames + +#endif // COCKATRICE_COUNTER_IDS_H diff --git a/libcockatrice_utility/libcockatrice/utility/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h index 833ce1b98..4b35a546e 100644 --- a/libcockatrice_utility/libcockatrice/utility/trice_limits.h +++ b/libcockatrice_utility/libcockatrice/utility/trice_limits.h @@ -15,11 +15,11 @@ constexpr uint MAXIMUM_DIE_SIDES = 1000000; constexpr uint MINIMUM_DICE_TO_ROLL = 1; constexpr uint MAXIMUM_DICE_TO_ROLL = 100; -// Card counter value bounds [0, MAX_COUNTERS_ON_CARD]. -// Counters on cards (e.g., +1/+1 counters, charge counters) are non-negative physical game objects. +// Counter value bounds [0, MAX_COUNTER_VALUE]. +// Counters (on cards or players) are non-negative values. // The max of 999 is a display constraint (3-digit rendering) and reasonable gameplay limit. // Server enforces these bounds; client may also check for UX optimization. -constexpr int MAX_COUNTERS_ON_CARD = 999; +constexpr int MAX_COUNTER_VALUE = 999; // optimized functions to get qstrings that are at most that long static inline QString nameFromStdString(const std::string &_string) diff --git a/libcockatrice_utility/libcockatrice/utility/zone_names.h b/libcockatrice_utility/libcockatrice/utility/zone_names.h index d1463de6a..0388cdf71 100644 --- a/libcockatrice_utility/libcockatrice/utility/zone_names.h +++ b/libcockatrice_utility/libcockatrice/utility/zone_names.h @@ -14,6 +14,9 @@ constexpr const char *DECK = "deck"; constexpr const char *SIDEBOARD = "sb"; constexpr const char *STACK = "stack"; +// Command zone (Commander format) +constexpr const char *COMMAND = "command"; + } // namespace ZoneNames #endif // ZONE_NAMES_H diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp index 2231a7e3b..aa0de591f 100644 --- a/tests/movecard_tests/reverse_card_move_test.cpp +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -22,7 +22,8 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest) // instantiate a fake server instance FakeServer server; Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); + Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, false, + &room); Server_AbstractPlayer player(&game, 1, user, false, nullptr); Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); diff --git a/tests/server_card_counter_test.cpp b/tests/server_card_counter_test.cpp index ff906b906..b6aacc31b 100644 --- a/tests/server_card_counter_test.cpp +++ b/tests/server_card_counter_test.cpp @@ -28,9 +28,9 @@ TEST(ServerCardCounter, IncrementExistingCounter) TEST(ServerCardCounter, IncrementOverflowProtection) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); EXPECT_FALSE(card.incrementCounter(1, 1)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, DecrementUnderflowProtection) @@ -113,13 +113,13 @@ TEST(ServerCardCounter, IncrementCounterPopulatesEvent) TEST(ServerCardCounter, IncrementCounterEventReflectsClampedValue) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); Event_SetCardCounter event; EXPECT_TRUE(card.incrementCounter(1, 10, &event)); EXPECT_EQ(event.counter_id(), 1); - EXPECT_EQ(event.counter_value(), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(event.counter_value(), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) @@ -133,7 +133,7 @@ TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) TEST(ServerCardCounter, IncrementCounterEventNotPopulatedWhenUnchanged) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); Event_SetCardCounter event; event.set_counter_id(999); @@ -156,7 +156,7 @@ TEST(ServerCardCounter, SetCounterClampsAboveMaxToMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); EXPECT_TRUE(card.setCounter(1, 1500)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) @@ -171,9 +171,9 @@ TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) TEST(ServerCardCounter, IncrementDoesNotExceedMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); EXPECT_TRUE(card.incrementCounter(1, 10)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } int main(int argc, char **argv) diff --git a/tests/server_counter_test.cpp b/tests/server_counter_test.cpp index 0f41f2cbd..1f3c20154 100644 --- a/tests/server_counter_test.cpp +++ b/tests/server_counter_test.cpp @@ -5,6 +5,7 @@ #include #include +#include #include TEST(ServerCounter, IncrementDoesNotOverflow) @@ -79,6 +80,37 @@ TEST(ServerCounter, MixedExtremesDoNotClamp) EXPECT_EQ(c.getCount(), -1); } +TEST(ServerCounter, SetCountClampsToCustomBounds) +{ + Server_Counter c(1, "test", color(), 10, 50, 0, 100); + EXPECT_TRUE(c.setCount(150)); + EXPECT_EQ(c.getCount(), 100); + EXPECT_TRUE(c.setCount(-10)); + EXPECT_EQ(c.getCount(), 0); +} + +TEST(ServerCounter, IncrementClampsToCustomBounds) +{ + Server_Counter c(1, "test", color(), 10, 50, 0, 100); + EXPECT_TRUE(c.incrementCount(100)); + EXPECT_EQ(c.getCount(), 100); + EXPECT_FALSE(c.incrementCount(1)); + EXPECT_EQ(c.getCount(), 100); + EXPECT_TRUE(c.incrementCount(-200)); + EXPECT_EQ(c.getCount(), 0); + EXPECT_FALSE(c.incrementCount(-1)); + EXPECT_EQ(c.getCount(), 0); +} + +TEST(ServerCounter, CustomBoundsForCommanderTax) +{ + Server_Counter taxCounter(1, "tax", color(), 20, 0, 0, MAX_COUNTER_VALUE); + EXPECT_TRUE(taxCounter.setCount(1000)); + EXPECT_EQ(taxCounter.getCount(), MAX_COUNTER_VALUE); + EXPECT_TRUE(taxCounter.setCount(-5)); + EXPECT_EQ(taxCounter.getCount(), 0); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv);