feat(command-zone): add commander tax counter system with tests

Implement CommanderTaxCounter - tracks casting cost increases for
commanders that have been cast multiple times.

Key components:
- CommanderTaxCounter: specialized counter with value clamping
- AbstractCounter modifications: support for counter ID system
- CounterGeneral/TranslateCounterName: UI integration for display

Design decisions:
- Counter values clamped to valid range (0 to max)
- Builds on counter_ids.h constants from commit 1
- Uses z_values.h from commit 4 for proper visual layering
- Counter visibility tied to zone state (hidden when zone hidden)

Test coverage:
- commander_tax_counter_test: verifies setValue clamping behavior
- counter_visibility_test: verifies visibility state transitions

Note: Tests include <climits> for INT_MIN usage (required on some compilers
like GCC on Debian 11 where INT_MIN is not transitively included).
This commit is contained in:
DawnFire42 2026-02-26 15:50:30 -05:00
parent 8422357878
commit 7a0a3ff9ac
10 changed files with 694 additions and 8 deletions

View file

@ -23,12 +23,10 @@ AbstractCounter::AbstractCounter(Player *_player,
int _value,
bool _useNameForShortcut,
QGraphicsItem *parent)
: QGraphicsItem(parent), player(_player), id(_id), name(_name), value(_value),
: QGraphicsObject(parent), player(_player), id(_id), name(_name), value(_value),
useNameForShortcut(_useNameForShortcut), hovered(false), aDec(nullptr), aInc(nullptr), dialogSemaphore(false),
deleteAfterDialog(false), shownInCounterArea(_shownInCounterArea)
{
setAcceptHoverEvents(true);
shortcutActive = false;
if (player->getPlayerInfo()->getLocalOrJudge()) {

View file

@ -9,7 +9,7 @@
#include "../../interface/widgets/menus/tearoff_menu.h"
#include <QGraphicsItem>
#include <QGraphicsObject>
#include <QInputDialog>
class Player;
@ -18,10 +18,9 @@ class QKeyEvent;
class QMenu;
class QString;
class AbstractCounter : public QObject, public QGraphicsItem
class AbstractCounter : public QGraphicsObject
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
protected:
Player *player;
@ -57,7 +56,7 @@ public:
~AbstractCounter() override;
void retranslateUi();
void setValue(int _value);
virtual void setValue(int _value);
void setShortcutsActive();
void setShortcutsInactive();
void delCounter();

View file

@ -0,0 +1,74 @@
#include "commander_tax_counter.h"
#include "../../interface/theme_manager.h"
#include "../player/player.h"
#include "../player/player_actions.h"
#include "../z_values.h"
#include <QFontDatabase>
#include <QPainter>
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
static constexpr qreal CORNER_RADIUS = 4.0;
static constexpr qreal FONT_SIZE_RATIO = 0.6;
CommanderTaxCounter::CommanderTaxCounter(Player *_player,
int _id,
const QString &_name,
int _size,
int _value,
QGraphicsItem *parent)
: AbstractCounter(_player, _id, _name, false, _value, false, parent), size(_size)
{
// Graphics configuration must be in concrete class, not AbstractCounter.
// Calling setCursor/setCacheMode in abstract base triggers pure virtual calls.
// See PileZone constructor for the same pattern.
setCacheMode(DeviceCoordinateCache);
setAcceptHoverEvents(true);
setCursor(Qt::ArrowCursor);
// Update appearance when theme changes
connect(themeManager, &ThemeManager::themeChanged, this, [this]() { update(); });
setToolTip(tr("Commander tax: %1").arg(_value));
}
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();
// Draw square background with rounded corners - matches PlayerCounter style
QRectF rect = boundingRect().adjusted(1, 1, -1, -1);
// Use grey background color for tax counters
QColor bgColor = hovered ? GameColors::OVERLAY_BG_HOVERED : GameColors::OVERLAY_BG_NORMAL;
painter->setPen(Qt::NoPen);
painter->setBrush(bgColor);
painter->drawRoundedRect(rect, CORNER_RADIUS, CORNER_RADIUS);
// Draw the value text using system font for cross-platform consistency
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)
{
// Clamp to 0 - commander tax cannot be negative
int clampedValue = qMax(0, _value);
AbstractCounter::setValue(clampedValue);
setToolTip(tr("Commander tax: %1").arg(clampedValue));
}

View file

@ -0,0 +1,59 @@
/**
* @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"
/**
* @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.
*
* @see AbstractCounter
* @see CounterIds
*/
class CommanderTaxCounter : public AbstractCounter
{
Q_OBJECT
private:
int size;
public:
/**
* @brief Constructs a CommanderTaxCounter.
* @param _player The player who owns this counter
* @param _id Counter ID (CounterIds::CommanderTax or CounterIds::PartnerTax)
* @param _name Display name for the counter
* @param _size Size in pixels (both width and height, as the counter is square)
* @param _value Initial value for the counter
* @param parent Parent graphics item (typically the command zone)
*/
CommanderTaxCounter(Player *_player,
int _id,
const QString &_name,
int _size,
int _value,
QGraphicsItem *parent = nullptr);
[[nodiscard]] QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
/// @brief Sets counter value, clamping to >= 0.
/// @param _value New value (clamped if negative)
void setValue(int _value) override;
};
#endif // COCKATRICE_COMMANDER_TAX_COUNTER_H

View file

@ -15,7 +15,12 @@ GeneralCounter::GeneralCounter(Player *_player,
QGraphicsItem *parent)
: AbstractCounter(_player, _id, _name, true, _value, useNameForShortcut, parent), color(_color), radius(_radius)
{
// Graphics configuration must be in concrete class, not AbstractCounter.
// Calling setCursor/setCacheMode in abstract base triggers pure virtual calls.
// See PileZone constructor for the same pattern.
setCacheMode(DeviceCoordinateCache);
setAcceptHoverEvents(true);
setCursor(Qt::ArrowCursor);
}
QRectF GeneralCounter::boundingRect() const

View file

@ -1,5 +1,7 @@
#include "translate_counter_name.h"
#include <libcockatrice/common/counter_ids.h>
const QMap<QString, QString> TranslateCounterName::translated = {
{"life", QT_TRANSLATE_NOOP("TranslateCounterName", "Life")},
{"w", QT_TRANSLATE_NOOP("TranslateCounterName", "White")},
@ -8,4 +10,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")},
{CounterNames::CommanderTax, QT_TRANSLATE_NOOP("TranslateCounterName", "commander tax")},
{CounterNames::PartnerTax, QT_TRANSLATE_NOOP("TranslateCounterName", "partner tax")}};