[Game] Add Command Zone support with commander tax tracking

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

Took 9 minutes

Took 11 minutes
This commit is contained in:
DawnFire42 2026-05-21 21:30:40 -04:00 committed by Lukas Brübach
parent da4ba222c0
commit b4057a865d
73 changed files with 1540 additions and 86 deletions

View file

@ -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

View file

@ -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<bool>(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<bool>(_notifyaboutupdate);

View file

@ -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<ReleaseChannel *> 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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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

View file

@ -8,4 +8,6 @@ const QMap<QString, QString> 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")}};

View file

@ -10,6 +10,7 @@
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/protocol/pb/context_mulligan.pb.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/zone_names.h>
#include <utility>
@ -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("<font class=\"blue\">%1</font>").arg(value);
int delta = value - oldValue;
QString counterDisplayName = TranslateCounterName::getDisplayName(counterName);
QString taxLabel = QString("<font class=\"blue\">%1</font>").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()))

View file

@ -22,7 +22,8 @@ enum CardMenuActionType
cmMoveToHand,
cmMoveToGraveyard,
cmMoveToExile,
cmMoveToTable
cmMoveToTable,
cmMoveToCommandZone
};
#endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H

View file

@ -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 <QPainter>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/zone_names.h>
/**
@ -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"));

View file

@ -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;

View file

@ -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 <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/zone_names.h>
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());
}
}

View file

@ -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 <QMenu>
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

View file

@ -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"));
}

View file

@ -26,6 +26,7 @@ public:
QAction *aMoveToTable = nullptr;
QAction *aMoveToGraveyard = nullptr;
QAction *aMoveToExile = nullptr;
QAction *aMoveToCommandZone = nullptr;
};
#endif // COCKATRICE_MOVE_MENU_H

View file

@ -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<SideboardMenu>(player, playerMenu);
commandZoneMenu = addManagedMenu<CommandZoneMenu>(player, playerMenu);
auto updateCommandZoneMenuVisibility = [this](bool has) {
if (commandZoneMenu) {
commandZoneMenu->menuAction()->setVisible(has);
}
};
connect(player, &PlayerLogic::commandZoneSupportChanged, this, updateCommandZoneMenuVisibility);
updateCommandZoneMenuVisibility(player->hasServerCommandZone());
customZonesMenu = addManagedMenu<CustomZoneMenu>(player);
playerMenu->addSeparator();
@ -39,6 +50,7 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
utilityMenu = createManagedComponent<UtilityMenu>(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);
}
}
}

View file

@ -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

View file

@ -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 <QGraphicsView>
#include "../z_values.h"
#include "../zones/command_zone.h"
#include <libcockatrice/utility/counter_ids.h>
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
{
@ -118,6 +123,12 @@ void PlayerGraphicsItem::initializeZones()
new HandZone(player->getHandZone(), static_cast<int>(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<AbstractCounter *> PlayerGraphicsItem::getTaxCounterWidgets() const
{
QList<AbstractCounter *> 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();
}

View file

@ -12,6 +12,7 @@
#include <QGraphicsObject>
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<AbstractCounter *> 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();

View file

@ -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);

View file

@ -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);
}

View file

@ -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);
}

View file

@ -38,7 +38,8 @@ public:
Stack,
Table,
Player,
MaxRole = Player,
Command,
MaxRole = Command,
};
private:

View file

@ -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<int, QString> &_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 QMap<int, QS
createGameAsSpectatorCheckBox->setEnabled(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<int, QRadioButton *> 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<int, QRadioButton *> 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);

View file

@ -48,6 +48,7 @@ private:
QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox,
*spectatorsSeeEverythingCheckBox, *createGameAsJudgeCheckBox, *createGameAsSpectatorCheckBox;
QCheckBox *shareDecklistsOnLoadCheckBox;
QCheckBox *enableCommandZoneCheckBox;
QDialogButtonBox *buttonBox;
QPushButton *clearButton;
QCheckBox *rememberGameSettings;

View file

@ -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(),
};
}

View file

@ -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;

View file

@ -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());

View file

@ -171,6 +171,7 @@ void MainWindow::startLocalGame(const LocalGameOptions &options)
Command_CreateGame createCommand;
createCommand.set_max_players(static_cast<google::protobuf::uint32>(options.numberPlayers));
createCommand.set_starting_life_total(options.startingLifeTotal);
createCommand.set_enable_command_zone(options.enableCommandZone);
mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -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.
*/
/**

View file

@ -37,6 +37,7 @@
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter_active.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
@ -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;
}

View file

@ -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);

View file

@ -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<int64_t>(oldValue) + static_cast<int64_t>(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<int>(qBound(static_cast<int64_t>(0), result, static_cast<int64_t>(MAX_COUNTERS_ON_CARD)));
static_cast<int>(qBound(static_cast<int64_t>(0), result, static_cast<int64_t>(MAX_COUNTER_VALUE)));
if (newValue == oldValue) {
return false;

View file

@ -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)

View file

@ -1,24 +1,19 @@
#include "server_counter.h"
#include <libcockatrice/protocol/pb/serverinfo_counter.pb.h>
#include <limits>
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<int64_t>(count) + static_cast<int64_t>(delta);
count = static_cast<int>(qBound(static_cast<int64_t>(std::numeric_limits<int>::min()), result,
static_cast<int64_t>(std::numeric_limits<int>::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);
}

View file

@ -22,18 +22,18 @@
#include <QString>
#include <libcockatrice/protocol/pb/color.pb.h>
#include <limits>
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<int>::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<int>::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<int64_t>(count) + static_cast<int64_t>(delta);
const int clamped =
static_cast<int>(qBound(static_cast<int64_t>(minValue), result, static_cast<int64_t>(maxValue)));
int oldCount = count;
count = clamped;
return count != oldCount;
}
/**
* @brief Populates info with this counter's current state for network serialization.

View file

@ -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());

View file

@ -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;

View file

@ -27,6 +27,7 @@
#include <libcockatrice/protocol/pb/command_mulligan.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter_active.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
@ -39,6 +40,7 @@
#include <libcockatrice/protocol/pb/event_game_log_notice.pb.h>
#include <libcockatrice/protocol/pb/event_player_properties_changed.pb.h>
#include <libcockatrice/protocol/pb/event_set_counter.pb.h>
#include <libcockatrice/protocol/pb/event_set_counter_active.pb.h>
#include <libcockatrice/protocol/pb/event_shuffle.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
@ -47,6 +49,7 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/zone_names.h>
@ -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*/)
{

View file

@ -9,6 +9,7 @@ class Server_Player : public Server_AbstractPlayer
private:
QMap<int, Server_Counter *> counters;
QList<int> 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;

View file

@ -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);

View file

@ -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

View file

@ -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];
}

View file

@ -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;
}

View file

@ -38,6 +38,7 @@ message GameCommand {
UNCONCEDE = 1032;
JUDGE = 1033;
REVERSE_TURN = 1034;
SET_COUNTER_ACTIVE = 1035;
}
extensions 100 to max;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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];
}

View file

@ -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
)

View file

@ -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 <QString>
/**
* 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

View file

@ -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)

View file

@ -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

View file

@ -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<int>(), false, false, false, false, false, false, 20, false, &room);
Server_Game game(user, 1, "", "", 2, QList<int>(), 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);

View file

@ -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)

View file

@ -5,6 +5,7 @@
#include <gtest/gtest.h>
#include <libcockatrice/network/server/remote/game/server_counter.h>
#include <libcockatrice/utility/trice_limits.h>
#include <limits>
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);