Refactor tax counters into cast counts with separate ID space and user activated create/delete

This commit is contained in:
DawnFire42 2026-08-10 22:28:00 -04:00
parent 4354191918
commit f5e49a571e
No known key found for this signature in database
GPG key ID: 24BB855EE2911B33
50 changed files with 900 additions and 828 deletions

View file

@ -0,0 +1 @@
---

View file

@ -0,0 +1,3 @@
Start testing: Aug 10 22:25 EDT
----------------------------------------------------------
End testing: Aug 10 22:25 EDT

View file

@ -67,7 +67,7 @@ set(cockatrice_SOURCES
src/game_graphics/board/card_item.cpp src/game_graphics/board/card_item.cpp
src/game/board/card_list.cpp src/game/board/card_list.cpp
src/game/board/card_state.cpp src/game/board/card_state.cpp
src/game_graphics/board/commander_tax_counter.cpp src/game_graphics/board/cast_count_widget.cpp
src/game_graphics/board/counter_general.cpp src/game_graphics/board/counter_general.cpp
src/game/board/counter_state.cpp src/game/board/counter_state.cpp
src/game_graphics/board/translate_counter_name.cpp src/game_graphics/board/translate_counter_name.cpp

View file

@ -597,16 +597,16 @@ private:
{"Player/aViewBottomCards", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Bottom Cards of Library"), {"Player/aViewBottomCards", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Bottom Cards of Library"),
parseSequenceString("Ctrl+Shift+W"), parseSequenceString("Ctrl+Shift+W"),
ShortcutGroup::View)}, ShortcutGroup::View)},
{"Player/aAddCommanderTax", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Increase 1st Tax"), {"Player/aIncrease1stCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Increase 1st Cast Count"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Player_Counters)}, ShortcutGroup::Player_Counters)},
{"Player/aRemoveCommanderTax", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Decrease 1st Tax"), {"Player/aDecrease1stCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Decrease 1st Cast Count"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Player_Counters)}, ShortcutGroup::Player_Counters)},
{"Player/aAddPartnerTax", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Increase 2nd Tax"), {"Player/aIncrease2ndCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Increase 2nd Cast Count"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Player_Counters)}, ShortcutGroup::Player_Counters)},
{"Player/aRemovePartnerTax", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Decrease 2nd Tax"), {"Player/aDecrease2ndCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Decrease 2nd Cast Count"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Player_Counters)}, ShortcutGroup::Player_Counters)},
{"Player/aCloseMostRecentZoneView", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Close Recent View"), {"Player/aCloseMostRecentZoneView", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Close Recent View"),

View file

@ -14,10 +14,13 @@
#include <libcockatrice/card/relation/card_relation.h> #include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/protocol/pb/command_attach_card.pb.h> #include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h> #include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h>
#include <libcockatrice/protocol/pb/command_create_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_create_token.pb.h> #include <libcockatrice/protocol/pb/command_create_token.pb.h>
#include <libcockatrice/protocol/pb/command_delete_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h> #include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_flip_card.pb.h> #include <libcockatrice/protocol/pb/command_flip_card.pb.h>
#include <libcockatrice/protocol/pb/command_game_say.pb.h> #include <libcockatrice/protocol/pb/command_game_say.pb.h>
#include <libcockatrice/protocol/pb/command_inc_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h> #include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h> #include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/command_mulligan.pb.h> #include <libcockatrice/protocol/pb/command_mulligan.pb.h>
@ -25,7 +28,6 @@
#include <libcockatrice/protocol/pb/command_roll_die.pb.h> #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_attr.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_counter.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_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h> #include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h> #include <libcockatrice/protocol/pb/context_move_card.pb.h>
@ -1674,32 +1676,34 @@ void PlayerActions::playSelectedCardsImpl(
} }
} }
void PlayerActions::actPlayAndIncrease1stTax(QList<CardItem *> selectedCards) void PlayerActions::actPlayAndIncrease1stCastCount(QList<CardItem *> selectedCards)
{ {
playAndIncreaseTax(selectedCards, CounterIds::TaxCounter1); playAndIncreaseCastCount(selectedCards, 1);
} }
void PlayerActions::actPlayAndIncrease2ndTax(QList<CardItem *> selectedCards) void PlayerActions::actPlayAndIncrease2ndCastCount(QList<CardItem *> selectedCards)
{ {
playAndIncreaseTax(selectedCards, CounterIds::TaxCounter2); playAndIncreaseCastCount(selectedCards, 2);
} }
void PlayerActions::playAndIncreaseTax(QList<CardItem *> selectedCards, int counterId) void PlayerActions::playAndIncreaseCastCount(QList<CardItem *> selectedCards, int index)
{ {
playSelectedCardsImpl(selectedCards, false, [this, counterId](PendingCommand *pend, const QString &originalZone) { playSelectedCardsImpl(selectedCards, false, [this, index](PendingCommand *pend, const QString &originalZone) {
if (originalZone != ZoneNames::COMMAND || pend == nullptr) { if (originalZone != ZoneNames::COMMAND || pend == nullptr) {
return; return;
} }
// Gate the tax increment on the server accepting the move, so a rejected move // Gate the increment on the server accepting the move
// (the card couldn't legally leave the command zone) never inflates the tax.
connect(pend, &PendingCommand::finished, this, connect(pend, &PendingCommand::finished, this,
[this, counterId](const Response &response, const CommandContainer &, const QVariant &) { [this, index](const Response &response, const CommandContainer &, const QVariant &) {
if (response.response_code() != Response::RespOk) { if (response.response_code() != Response::RespOk) {
return; return;
} }
CounterState *state = player->getCounters().value(counterId, nullptr); CounterState *state = player->getCastCount(index);
if (state && state->isActive()) { if (state) {
sendIncCounter(counterId, 1); Command_IncCastCount cmd;
cmd.set_index(index);
cmd.set_delta(1);
sendGameCommand(cmd);
} }
}); });
}); });
@ -1713,29 +1717,40 @@ void PlayerActions::sendIncCounter(int counterId, int delta)
sendGameCommand(cmd); sendGameCommand(cmd);
} }
void PlayerActions::actModifyTaxCounter(int counterId, int delta) void PlayerActions::actModifyCastCount(int index, int delta)
{ {
CounterState *state = player->getCounters().value(counterId, nullptr); if (!CastCountIds::isValidIndex(index)) {
if (!state || !state->isActive() || !CounterIds::isTaxCounter(counterId)) {
return; return;
} }
sendIncCounter(counterId, delta); CounterState *state = player->getCastCount(index);
if (!state) {
return;
}
Command_IncCastCount cmd;
cmd.set_index(index);
cmd.set_delta(delta);
sendGameCommand(cmd);
} }
void PlayerActions::actToggleTaxCounter(int counterId) void PlayerActions::actToggleCastCount(int index)
{ {
CounterState *state = player->getCounters().value(counterId, nullptr); if (!CastCountIds::isValidIndex(index)) {
if (!state || !CounterIds::isTaxCounter(counterId)) {
return; return;
} }
// Prevent disabling a counter with tax accumulated; player must reset to 0 first CounterState *state = player->getCastCount(index);
if (state->isActive() && state->getValue() != 0) { if (state) {
// Delete only allowed when value is 0
if (state->getValue() != 0) {
return; return;
} }
Command_SetCounterActive cmd; Command_DeleteCastCount cmd;
cmd.set_counter_id(counterId); cmd.set_index(index);
cmd.set_active(!state->isActive());
sendGameCommand(cmd); sendGameCommand(cmd);
} else {
Command_CreateCastCount cmd;
cmd.set_index(index);
sendGameCommand(cmd);
}
} }
void PlayerActions::actPlay(QList<CardItem *> selectedCards) void PlayerActions::actPlay(QList<CardItem *> selectedCards)

View file

@ -127,14 +127,14 @@ public slots:
void actPlay(QList<CardItem *> selectedCards); void actPlay(QList<CardItem *> selectedCards);
void actPlayFacedown(QList<CardItem *> selectedCards); void actPlayFacedown(QList<CardItem *> selectedCards);
/** @brief Plays the selected card and increments the 1st tax counter. */ /** @brief Plays the selected card and increments the 1st cast count. */
void actPlayAndIncrease1stTax(QList<CardItem *> selectedCards); void actPlayAndIncrease1stCastCount(QList<CardItem *> selectedCards);
/** @brief Plays the selected card and increments the 2nd tax counter. */ /** @brief Plays the selected card and increments the 2nd cast count. */
void actPlayAndIncrease2ndTax(QList<CardItem *> selectedCards); void actPlayAndIncrease2ndCastCount(QList<CardItem *> selectedCards);
/** @brief Modifies a tax counter by delta if it is active. */ /** @brief Modifies a cast count by delta. */
void actModifyTaxCounter(int counterId, int delta); void actModifyCastCount(int index, int delta);
/** @brief Toggles a tax counter's active state (only if inactive or value is 0). */ /** @brief Toggles a cast count's existence (create if missing, delete if value is 0). */
void actToggleTaxCounter(int counterId); void actToggleCastCount(int index);
void actHide(QList<CardItem *> selectedCards); void actHide(QList<CardItem *> selectedCards);
void actMoveTopCardToPlay(); void actMoveTopCardToPlay();
@ -280,11 +280,11 @@ private:
/** /**
* @brief Plays the selected cards and, for each that came from the command zone and whose move * @brief Plays the selected cards and, for each that came from the command zone and whose move
* the server accepts, increments the given (active) tax counter by one. * the server accepts, increments the given cast count by one.
* @param selectedCards Cards to play * @param selectedCards Cards to play
* @param counterId The tax counter to increment (CounterIds::TaxCounter1 through TaxCounter5) * @param index The cast count index (1-5)
*/ */
void playAndIncreaseTax(QList<CardItem *> selectedCards, int counterId); void playAndIncreaseCastCount(QList<CardItem *> selectedCards, int index);
void cmdSetTopCard(Command_MoveCard &cmd); void cmdSetTopCard(Command_MoveCard &cmd);
void cmdSetBottomCard(Command_MoveCard &cmd); void cmdSetBottomCard(Command_MoveCard &cmd);

View file

@ -15,10 +15,12 @@
#include <libcockatrice/protocol/pb/event_attach_card.pb.h> #include <libcockatrice/protocol/pb/event_attach_card.pb.h>
#include <libcockatrice/protocol/pb/event_change_zone_properties.pb.h> #include <libcockatrice/protocol/pb/event_change_zone_properties.pb.h>
#include <libcockatrice/protocol/pb/event_create_arrow.pb.h> #include <libcockatrice/protocol/pb/event_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/event_create_cast_count.pb.h>
#include <libcockatrice/protocol/pb/event_create_counter.pb.h> #include <libcockatrice/protocol/pb/event_create_counter.pb.h>
#include <libcockatrice/protocol/pb/event_create_token.pb.h> #include <libcockatrice/protocol/pb/event_create_token.pb.h>
#include <libcockatrice/protocol/pb/event_del_counter.pb.h> #include <libcockatrice/protocol/pb/event_del_counter.pb.h>
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h> #include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
#include <libcockatrice/protocol/pb/event_delete_cast_count.pb.h>
#include <libcockatrice/protocol/pb/event_destroy_card.pb.h> #include <libcockatrice/protocol/pb/event_destroy_card.pb.h>
#include <libcockatrice/protocol/pb/event_draw_cards.pb.h> #include <libcockatrice/protocol/pb/event_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/event_dump_zone.pb.h> #include <libcockatrice/protocol/pb/event_dump_zone.pb.h>
@ -30,8 +32,8 @@
#include <libcockatrice/protocol/pb/event_roll_die.pb.h> #include <libcockatrice/protocol/pb/event_roll_die.pb.h>
#include <libcockatrice/protocol/pb/event_set_card_attr.pb.h> #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_card_counter.pb.h>
#include <libcockatrice/protocol/pb/event_set_cast_count.pb.h>
#include <libcockatrice/protocol/pb/event_set_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/protocol/pb/event_shuffle.pb.h>
#include <libcockatrice/utility/color.h> #include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
@ -277,20 +279,26 @@ void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
} }
} }
void PlayerEventHandler::eventSetCounterActive(const Event_SetCounterActive &event)
{
CounterState *state = player->getCounters().value(event.counter_id(), nullptr);
if (!state) {
return;
}
state->setActive(event.active());
}
void PlayerEventHandler::eventDelCounter(const Event_DelCounter &event) void PlayerEventHandler::eventDelCounter(const Event_DelCounter &event)
{ {
player->delCounter(event.counter_id()); player->delCounter(event.counter_id());
} }
void PlayerEventHandler::eventCreateCastCount(const Event_CreateCastCount &event)
{
player->addCastCount(event.index());
}
void PlayerEventHandler::eventDeleteCastCount(const Event_DeleteCastCount &event)
{
player->delCastCount(event.index());
}
void PlayerEventHandler::eventSetCastCount(const Event_SetCastCount &event)
{
player->setCastCountValue(event.index(), event.value());
}
void PlayerEventHandler::eventDumpZone(const Event_DumpZone &event) void PlayerEventHandler::eventDumpZone(const Event_DumpZone &event)
{ {
PlayerLogic *zoneOwner = player->getGame()->getPlayerManager()->getPlayers().value(event.zone_owner_id(), 0); PlayerLogic *zoneOwner = player->getGame()->getPlayerManager()->getPlayers().value(event.zone_owner_id(), 0);
@ -640,12 +648,18 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
case GameEvent::SET_COUNTER: case GameEvent::SET_COUNTER:
eventSetCounter(event.GetExtension(Event_SetCounter::ext)); eventSetCounter(event.GetExtension(Event_SetCounter::ext));
break; break;
case GameEvent::SET_COUNTER_ACTIVE:
eventSetCounterActive(event.GetExtension(Event_SetCounterActive::ext));
break;
case GameEvent::DEL_COUNTER: case GameEvent::DEL_COUNTER:
eventDelCounter(event.GetExtension(Event_DelCounter::ext)); eventDelCounter(event.GetExtension(Event_DelCounter::ext));
break; break;
case GameEvent::CREATE_CAST_COUNT:
eventCreateCastCount(event.GetExtension(Event_CreateCastCount::ext));
break;
case GameEvent::DELETE_CAST_COUNT:
eventDeleteCastCount(event.GetExtension(Event_DeleteCastCount::ext));
break;
case GameEvent::SET_CAST_COUNT:
eventSetCastCount(event.GetExtension(Event_SetCastCount::ext));
break;
case GameEvent::DUMP_ZONE: case GameEvent::DUMP_ZONE:
eventDumpZone(event.GetExtension(Event_DumpZone::ext)); eventDumpZone(event.GetExtension(Event_DumpZone::ext));
break; break;

View file

@ -48,7 +48,9 @@ class Event_RollDie;
class Event_SetCardAttr; class Event_SetCardAttr;
class Event_SetCardCounter; class Event_SetCardCounter;
class Event_SetCounter; class Event_SetCounter;
class Event_SetCounterActive; class Event_CreateCastCount;
class Event_DeleteCastCount;
class Event_SetCastCount;
class Event_Shuffle; class Event_Shuffle;
class Event_GameLogNotice; class Event_GameLogNotice;
@ -156,12 +158,18 @@ public:
/// Set a player-level counter value. /// Set a player-level counter value.
void eventSetCounter(const Event_SetCounter &event); void eventSetCounter(const Event_SetCounter &event);
/** @brief Show or hide a player-level counter without deleting it. */
void eventSetCounterActive(const Event_SetCounterActive &event);
/// Delete a player-level counter. /// Delete a player-level counter.
void eventDelCounter(const Event_DelCounter &event); void eventDelCounter(const Event_DelCounter &event);
/// Create a cast count.
void eventCreateCastCount(const Event_CreateCastCount &event);
/// Delete a cast count.
void eventDeleteCastCount(const Event_DeleteCastCount &event);
/// Set a cast count value.
void eventSetCastCount(const Event_SetCastCount &event);
/** @} */ /** @} */
/** @name Zone-level operations /** @name Zone-level operations

View file

@ -292,6 +292,49 @@ void PlayerLogic::clearCounters()
} }
qDeleteAll(counters); qDeleteAll(counters);
counters.clear(); counters.clear();
for (int index : castCounts.keys()) {
emit castCountRemoved(index);
}
qDeleteAll(castCounts);
castCounts.clear();
}
CounterState *PlayerLogic::addCastCount(int index)
{
if (castCounts.contains(index)) {
return nullptr;
}
QString name = CastCountIds::nameForIndex(index);
auto *state = new CounterState(index, name, QColor(128, 128, 128), 20, 0, true, this);
castCounts.insert(index, state);
emit castCountAdded(index, state);
return state;
}
void PlayerLogic::delCastCount(int index)
{
auto *state = castCounts.take(index);
if (!state) {
return;
}
emit castCountRemoved(index);
state->deleteLater();
}
void PlayerLogic::setCastCountValue(int index, int value)
{
auto *state = castCounts.value(index, nullptr);
if (!state) {
return;
}
state->setValue(value);
emit castCountValueChanged(index, value);
}
CounterState *PlayerLogic::getCastCount(int index) const
{
return castCounts.value(index, nullptr);
} }
CounterState *PlayerLogic::getLifeCounter() const CounterState *PlayerLogic::getLifeCounter() const

View file

@ -79,6 +79,9 @@ signals:
void counterAdded(CounterState *state); void counterAdded(CounterState *state);
void counterRemoved(int counterId); void counterRemoved(int counterId);
void rearrangeCounters(); void rearrangeCounters();
void castCountAdded(int index, CounterState *state);
void castCountRemoved(int index);
void castCountValueChanged(int index, int value);
void activeChanged(bool active); void activeChanged(bool active);
void zoneIdChanged(int zoneId); void zoneIdChanged(int zoneId);
void concededChanged(int playerId, bool conceded); void concededChanged(int playerId, bool conceded);
@ -225,6 +228,16 @@ public:
*/ */
CounterState *getLifeCounter() const; CounterState *getLifeCounter() const;
// Cast count management
CounterState *addCastCount(int index);
void delCastCount(int index);
void setCastCountValue(int index, int value);
CounterState *getCastCount(int index) const;
const QMap<int, CounterState *> &getCastCounts() const
{
return castCounts;
}
void setConceded(bool _conceded); void setConceded(bool _conceded);
bool getConceded() const bool getConceded() const
{ {
@ -259,6 +272,7 @@ private:
int zoneId; int zoneId;
QMap<QString, CardZoneLogic *> zones; QMap<QString, CardZoneLogic *> zones;
QMap<int, CounterState *> counters; QMap<int, CounterState *> counters;
QMap<int, CounterState *> castCounts; // index (1-5) -> state
bool dialogSemaphore; bool dialogSemaphore;
QList<CardItem *> cardsToDelete; QList<CardItem *> cardsToDelete;

View file

@ -30,7 +30,7 @@ AbstractCounter::AbstractCounter(CounterState *state,
setAcceptHoverEvents(true); setAcceptHoverEvents(true);
// Route through the (possibly overridden) virtual setValue so subclasses such as // Route through the (possibly overridden) virtual setValue so subclasses such as
// CommanderTaxCounter can clamp and refresh their tooltip on every value change. // CastCountWidget can clamp and refresh their tooltip on every value change.
connect(state, &CounterState::valueChanged, this, [this](int, int newValue) { setValue(newValue); }); connect(state, &CounterState::valueChanged, this, [this](int, int newValue) { setValue(newValue); });
connect(state, &CounterState::activeChanged, this, [this](bool newActive) { connect(state, &CounterState::activeChanged, this, [this](bool newActive) {

View file

@ -74,7 +74,7 @@ public:
/** /**
* @brief Sets the counter value and triggers a visual update. * @brief Sets the counter value and triggers a visual update.
* *
* Virtual to allow subclass display customization (e.g., CommanderTaxCounter tooltip updates). * Virtual to allow subclass display customization (e.g., CastCountWidget tooltip updates).
* Overflow protection is handled server-side, not in client counter classes. * Overflow protection is handled server-side, not in client counter classes.
* *
* @param _value The new counter value * @param _value The new counter value
@ -122,9 +122,9 @@ public:
/** /**
* @brief Shows or hides the counter and enables or disables its menu. * @brief Shows or hides the counter and enables or disables its menu.
* *
* Sole owner of both, so container layout code only positions counters and may read * Sole owner of both visibility and menu state, so container layout code only positions
* isActive(). Qt AND-s visibility with the parent item's, so an active counter nested in a * counters and may read isActive(). Note that Qt AND-s visibility with the parent item's,
* hidden zone (e.g. a tax counter in the command zone) still does not render. * so an active counter nested in a hidden parent still does not render.
* *
* @param _active True to show and enable the counter, false to hide and disable it * @param _active True to show and enable the counter, false to hide and disable it
*/ */

View file

@ -1,30 +1,28 @@
#include "commander_tax_counter.h" #include "cast_count_widget.h"
#include "../../game/board/counter_state.h" #include "../../game/board/counter_state.h"
#include "translate_counter_name.h" #include "translate_counter_name.h"
#include <QColor>
#include <QFontDatabase> #include <QFontDatabase>
#include <QPainter> #include <QPainter>
static constexpr qreal CORNER_RADIUS = 4.0; static constexpr qreal CORNER_RADIUS = 4.0;
static constexpr qreal FONT_SIZE_RATIO = 0.6; static constexpr qreal FONT_SIZE_RATIO = 0.6;
CommanderTaxCounter::CommanderTaxCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent) CastCountWidget::CastCountWidget(CounterState *state, PlayerLogic *player, QGraphicsItem *parent)
: AbstractCounter(state, player, false, false, parent), size(state->getRadius()) : AbstractCounter(state, player, false, false, parent), size(state->getRadius())
{ {
setCacheMode(DeviceCoordinateCache); setCacheMode(DeviceCoordinateCache);
setAcceptHoverEvents(true); setAcceptHoverEvents(true);
setToolTip(tr("%1: %2").arg(TranslateCounterName::getDisplayName(getName())).arg(getValue())); setToolTip(tr("%1: %2").arg(TranslateCounterName::getDisplayName(getName())).arg(getValue()));
} }
QRectF CommanderTaxCounter::boundingRect() const QRectF CastCountWidget::boundingRect() const
{ {
return QRectF(0, 0, size, size); return QRectF(0, 0, size, size);
} }
void CommanderTaxCounter::paint(QPainter *painter, void CastCountWidget::paint(QPainter *painter,
[[maybe_unused]] const QStyleOptionGraphicsItem *option, [[maybe_unused]] const QStyleOptionGraphicsItem *option,
[[maybe_unused]] QWidget *widget) [[maybe_unused]] QWidget *widget)
{ {
@ -46,7 +44,7 @@ void CommanderTaxCounter::paint(QPainter *painter,
painter->restore(); painter->restore();
} }
void CommanderTaxCounter::setValue(int _value) void CastCountWidget::setValue(int _value)
{ {
int clampedValue = qMax(0, _value); int clampedValue = qMax(0, _value);
AbstractCounter::setValue(clampedValue); AbstractCounter::setValue(clampedValue);

View file

@ -0,0 +1,35 @@
/**
* @file cast_count_widget.h
* @ingroup GameGraphicsBoard
* @brief Displays a single cast count overlay counter in the command zone.
*/
#ifndef COCKATRICE_CAST_COUNT_WIDGET_H
#define COCKATRICE_CAST_COUNT_WIDGET_H
#include "abstract_counter.h"
namespace CastCountSizes
{
constexpr int MARGIN = 2;
} // namespace CastCountSizes
/**
* @class CastCountWidget
* @brief A compact counter widget for tracking cast counts in the command zone.
*/
class CastCountWidget : public AbstractCounter
{
Q_OBJECT
private:
int size;
public:
CastCountWidget(CounterState *state, PlayerLogic *player, QGraphicsItem *parent = nullptr);
[[nodiscard]] QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
void setValue(int _value) override;
};
#endif // COCKATRICE_CAST_COUNT_WIDGET_H

View file

@ -1,70 +0,0 @@
/**
* @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 Layout constants for commander tax counters.
*/
namespace TaxCounterSizes
{
/** @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 the number of times the commander has been cast from the command
* zone. Can be adjusted manually via +1/-1 menu actions, or automatically
* incremented when using "Play and Increase Tax" on an accepted cast from
* the command zone. 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; ///< Width and height of the counter in pixels
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

@ -1,7 +1,5 @@
#include "translate_counter_name.h" #include "translate_counter_name.h"
#include <libcockatrice/utility/counter_ids.h>
const QMap<QString, QString> TranslateCounterName::translated = { const QMap<QString, QString> TranslateCounterName::translated = {
{"life", QT_TRANSLATE_NOOP("TranslateCounterName", "Life")}, {"life", QT_TRANSLATE_NOOP("TranslateCounterName", "Life")},
{"w", QT_TRANSLATE_NOOP("TranslateCounterName", "White")}, {"w", QT_TRANSLATE_NOOP("TranslateCounterName", "White")},
@ -11,8 +9,8 @@ const QMap<QString, QString> TranslateCounterName::translated = {
{"g", QT_TRANSLATE_NOOP("TranslateCounterName", "Green")}, {"g", QT_TRANSLATE_NOOP("TranslateCounterName", "Green")},
{"x", QT_TRANSLATE_NOOP("TranslateCounterName", "Colorless")}, {"x", QT_TRANSLATE_NOOP("TranslateCounterName", "Colorless")},
{"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}, {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")},
{CounterNames::TaxCounter1, QT_TRANSLATE_NOOP("TranslateCounterName", "1st Tax")}, {"cast_count_1", QT_TRANSLATE_NOOP("TranslateCounterName", "1st Cast Count")},
{CounterNames::TaxCounter2, QT_TRANSLATE_NOOP("TranslateCounterName", "2nd Tax")}, {"cast_count_2", QT_TRANSLATE_NOOP("TranslateCounterName", "2nd Cast Count")},
{CounterNames::TaxCounter3, QT_TRANSLATE_NOOP("TranslateCounterName", "3rd Tax")}, {"cast_count_3", QT_TRANSLATE_NOOP("TranslateCounterName", "3rd Cast Count")},
{CounterNames::TaxCounter4, QT_TRANSLATE_NOOP("TranslateCounterName", "4th Tax")}, {"cast_count_4", QT_TRANSLATE_NOOP("TranslateCounterName", "4th Cast Count")},
{CounterNames::TaxCounter5, QT_TRANSLATE_NOOP("TranslateCounterName", "5th Tax")}}; {"cast_count_5", QT_TRANSLATE_NOOP("TranslateCounterName", "5th Cast Count")}};

View file

@ -672,27 +672,11 @@ void MessageLogWidget::logSetCardCounter(PlayerLogic *player, QString cardName,
void MessageLogWidget::logSetCounter(PlayerLogic *player, int counterId, QString counterName, int value, int oldValue) void MessageLogWidget::logSetCounter(PlayerLogic *player, int counterId, QString counterName, int value, int oldValue)
{ {
Q_UNUSED(counterId);
if (counterName == "life") { if (counterName == "life") {
soundEngine->playSound("life_change"); soundEngine->playSound("life_change");
} }
if (CounterIds::isTaxCounter(counterId)) {
QString playerName = sanitizeHtml(player->getPlayerInfo()->getName());
QString valueStr = QString("<font class=\"blue\">%1</font>").arg(value);
int delta = value - oldValue;
QString deltaStr = QString::number(delta);
QString counterDisplayName = TranslateCounterName::getDisplayName(counterName);
QString taxLabel = QString("<font class=\"blue\">%1</font>").arg(sanitizeHtml(counterDisplayName));
if (value > oldValue) {
// delta > 0 here; the format string supplies the leading '+'
appendHtmlServerMessage(tr("%1 increases %2 to %3 (+%4).").arg(playerName, taxLabel, valueStr, deltaStr));
} else {
// delta < 0 here; deltaStr already carries the '-' sign
appendHtmlServerMessage(tr("%1 decreases %2 to %3 (%4).").arg(playerName, taxLabel, valueStr, deltaStr));
}
return;
}
QString counterDisplayName = TranslateCounterName::getDisplayName(counterName); QString counterDisplayName = TranslateCounterName::getDisplayName(counterName);
appendHtmlServerMessage(tr("%1 sets counter %2 to %3 (%4%5).") appendHtmlServerMessage(tr("%1 sets counter %2 to %3 (%4%5).")
.arg(sanitizeHtml(player->getPlayerInfo()->getName())) .arg(sanitizeHtml(player->getPlayerInfo()->getName()))

View file

@ -84,8 +84,10 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
aUnattach = makeAction(this, [actions, sel]() { actions->actUnattach(sel()); }); aUnattach = makeAction(this, [actions, sel]() { actions->actUnattach(sel()); });
aSetAnnotation = makeAction(this, [actions, sel]() { actions->actRequestSetAnnotationDialog(sel()); }); aSetAnnotation = makeAction(this, [actions, sel]() { actions->actRequestSetAnnotationDialog(sel()); });
aPlay = makeAction(this, [actions, sel]() { actions->actPlay(sel()); }); aPlay = makeAction(this, [actions, sel]() { actions->actPlay(sel()); });
aPlayAndIncrease1stTax = makeAction(this, [actions, sel]() { actions->actPlayAndIncrease1stTax(sel()); }); aPlayAndIncrease1stCastCount =
aPlayAndIncrease2ndTax = makeAction(this, [actions, sel]() { actions->actPlayAndIncrease2ndTax(sel()); }); makeAction(this, [actions, sel]() { actions->actPlayAndIncrease1stCastCount(sel()); });
aPlayAndIncrease2ndCastCount =
makeAction(this, [actions, sel]() { actions->actPlayAndIncrease2ndCastCount(sel()); });
aPlayFacedown = makeAction(this, [actions, sel]() { actions->actPlayFacedown(sel()); }); aPlayFacedown = makeAction(this, [actions, sel]() { actions->actPlayFacedown(sel()); });
aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); }); aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); });
aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); }); aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); });
@ -166,16 +168,15 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
if (writeableCard) { if (writeableCard) {
addAction(aPlay); addAction(aPlay);
// Only offer for single selection: a multi-select would over-count casts by // Only offer for single selection: a multi-select would over-count casts
// bumping one commander's tax counter once per command-zone card.
const bool singleSelection = gameScene->selectedCards().size() <= 1; const bool singleSelection = gameScene->selectedCards().size() <= 1;
if (singleSelection && player->getTaxCounterIfActive(CounterIds::TaxCounter1)) { if (singleSelection && player->getCastCountWidget(1)) {
addAction(aPlayAndIncrease1stTax); addAction(aPlayAndIncrease1stCastCount);
} }
if (singleSelection && player->getTaxCounterIfActive(CounterIds::TaxCounter2)) { if (singleSelection && player->getCastCountWidget(2)) {
addAction(aPlayAndIncrease2ndTax); addAction(aPlayAndIncrease2ndCastCount);
} }
// No reveal submenu - command zone is public // No reveal submenu - command zone is public
@ -524,8 +525,8 @@ void CardMenu::retranslateUi()
aPlay->setText(tr("&Play")); aPlay->setText(tr("&Play"));
aHide->setText(tr("&Hide")); aHide->setText(tr("&Hide"));
aPlayFacedown->setText(tr("Play &Face Down")); aPlayFacedown->setText(tr("Play &Face Down"));
aPlayAndIncrease1stTax->setText(tr("Play and &Increase 1st Tax")); aPlayAndIncrease1stCastCount->setText(tr("Play and &Increase 1st Cast Count"));
aPlayAndIncrease2ndTax->setText(tr("Play and Increase &2nd Tax")); aPlayAndIncrease2ndCastCount->setText(tr("Play and Increase &2nd Cast Count"));
aRevealToAll->setText(tr("&All players")); aRevealToAll->setText(tr("&All players"));
//: Turn sideways or back again //: Turn sideways or back again
aTap->setText(tr("&Tap / Untap")); aTap->setText(tr("&Tap / Untap"));

View file

@ -32,8 +32,8 @@ public:
QMenu *mCardCounters; QMenu *mCardCounters;
QAction *aPlay, *aPlayFacedown; QAction *aPlay, *aPlayFacedown;
/** @brief Play actions that also increment the corresponding tax counter. */ /** @brief Play actions that also increment the corresponding cast count. */
QAction *aPlayAndIncrease1stTax, *aPlayAndIncrease2ndTax; QAction *aPlayAndIncrease1stCastCount, *aPlayAndIncrease2ndCastCount;
QAction *aRevealToAll; QAction *aRevealToAll;
QAction *aHide; QAction *aHide;
QAction *aClone; QAction *aClone;

View file

@ -14,42 +14,41 @@
CommandZoneMenu::CommandZoneMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) CommandZoneMenu::CommandZoneMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
{ {
// Shortcuts only for first two tax counters (matching legacy behavior) inc1ShortcutKey = QStringLiteral("Player/aIncrease1stCastCount");
incTax1ShortcutKey = QStringLiteral("Player/aAddCommanderTax"); dec1ShortcutKey = QStringLiteral("Player/aDecrease1stCastCount");
decTax1ShortcutKey = QStringLiteral("Player/aRemoveCommanderTax"); inc2ShortcutKey = QStringLiteral("Player/aIncrease2ndCastCount");
incTax2ShortcutKey = QStringLiteral("Player/aAddPartnerTax"); dec2ShortcutKey = QStringLiteral("Player/aDecrease2ndCastCount");
decTax2ShortcutKey = QStringLiteral("Player/aRemovePartnerTax");
PlayerLogic *logic = player->getLogic(); PlayerLogic *logic = player->getLogic();
if (logic && logic->getPlayerInfo()->getLocalOrJudge()) { if (logic && logic->getPlayerInfo()->getLocalOrJudge()) {
for (int i = 0; i < TaxCounterCount; ++i) { for (int i = 0; i < CastCountCount; ++i) {
int counterId = CounterIds::taxCounterIdFromIndex(i); int index = i + 1; // 1-based index
aIncreaseTax[i] = new QAction(this); aIncrease[i] = new QAction(this);
connect(aIncreaseTax[i], &QAction::triggered, this, [this, counterId]() { connect(aIncrease[i], &QAction::triggered, this, [this, index]() {
if (auto *l = player->getLogic()) { if (auto *l = player->getLogic()) {
l->getPlayerActions()->actModifyTaxCounter(counterId, 1); l->getPlayerActions()->actModifyCastCount(index, 1);
} }
}); });
addAction(aIncreaseTax[i]); addAction(aIncrease[i]);
aDecreaseTax[i] = new QAction(this); aDecrease[i] = new QAction(this);
connect(aDecreaseTax[i], &QAction::triggered, this, [this, counterId]() { connect(aDecrease[i], &QAction::triggered, this, [this, index]() {
if (auto *l = player->getLogic()) { if (auto *l = player->getLogic()) {
l->getPlayerActions()->actModifyTaxCounter(counterId, -1); l->getPlayerActions()->actModifyCastCount(index, -1);
} }
}); });
addAction(aDecreaseTax[i]); addAction(aDecrease[i]);
addSeparator(); addSeparator();
aToggleTax[i] = new QAction(this); aToggle[i] = new QAction(this);
connect(aToggleTax[i], &QAction::triggered, this, [this, counterId]() { connect(aToggle[i], &QAction::triggered, this, [this, index]() {
if (auto *l = player->getLogic()) { if (auto *l = player->getLogic()) {
l->getPlayerActions()->actToggleTaxCounter(counterId); l->getPlayerActions()->actToggleCastCount(index);
} }
}); });
addAction(aToggleTax[i]); addAction(aToggle[i]);
addSeparator(); addSeparator();
} }
@ -58,7 +57,7 @@ CommandZoneMenu::CommandZoneMenu(PlayerGraphicsItem *_player, QMenu *playerMenu)
connect(aToggleMinimized, &QAction::triggered, this, &CommandZoneMenu::actToggleMinimized); connect(aToggleMinimized, &QAction::triggered, this, &CommandZoneMenu::actToggleMinimized);
addAction(aToggleMinimized); addAction(aToggleMinimized);
connect(this, &QMenu::aboutToShow, this, &CommandZoneMenu::updateTaxCounterActionStates); connect(this, &QMenu::aboutToShow, this, &CommandZoneMenu::updateCastCountActionStates);
} }
retranslateUi(); retranslateUi();
@ -70,14 +69,13 @@ void CommandZoneMenu::retranslateUi()
static const char *ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; static const char *ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
for (int i = 0; i < TaxCounterCount; ++i) { for (int i = 0; i < CastCountCount; ++i) {
if (aIncreaseTax[i]) { if (aIncrease[i]) {
aIncreaseTax[i]->setText(tr("&Increase %1 Tax (+1)").arg(ordinals[i])); aIncrease[i]->setText(tr("&Increase %1 Cast Count (+1)").arg(ordinals[i]));
} }
if (aDecreaseTax[i]) { if (aDecrease[i]) {
aDecreaseTax[i]->setText(tr("&Decrease %1 Tax (-1)").arg(ordinals[i])); aDecrease[i]->setText(tr("&Decrease %1 Cast Count (-1)").arg(ordinals[i]));
} }
// Toggle action labels are derived dynamically in updateTaxCounterActionStates()
} }
if (aToggleMinimized) { if (aToggleMinimized) {
@ -93,44 +91,39 @@ void CommandZoneMenu::actToggleMinimized()
} }
} }
void CommandZoneMenu::updateTaxCounterActionStates() void CommandZoneMenu::updateCastCountActionStates()
{ {
static const char *ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; static const char *ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
// Collect all tax counter states std::array<AbstractCounter *, CastCountCount> castCounts{};
std::array<AbstractCounter *, TaxCounterCount> taxCounters{}; for (int i = 0; i < CastCountCount; ++i) {
for (int i = 0; i < TaxCounterCount; ++i) { castCounts[i] = player->getCastCountWidget(i + 1);
taxCounters[i] = player->getTaxCounterIfActive(CounterIds::taxCounterIdFromIndex(i));
} }
// Find highest active tax counter index
int highestActive = -1; int highestActive = -1;
for (int i = TaxCounterCount - 1; i >= 0; --i) { for (int i = CastCountCount - 1; i >= 0; --i) {
if (taxCounters[i]) { if (castCounts[i]) {
highestActive = i; highestActive = i;
break; break;
} }
} }
for (int i = 0; i < TaxCounterCount; ++i) { for (int i = 0; i < CastCountCount; ++i) {
AbstractCounter *counter = taxCounters[i]; AbstractCounter *counter = castCounts[i];
if (aIncreaseTax[i]) { if (aIncrease[i]) {
aIncreaseTax[i]->setVisible(counter && counter->getValue() < MAX_COUNTER_VALUE); aIncrease[i]->setVisible(counter && counter->getValue() < MAX_COUNTER_VALUE);
} }
if (aDecreaseTax[i]) { if (aDecrease[i]) {
aDecreaseTax[i]->setVisible(counter && counter->getValue() > 0); aDecrease[i]->setVisible(counter && counter->getValue() > 0);
} }
if (aToggleTax[i]) { if (aToggle[i]) {
aToggleTax[i]->setText(counter ? tr("&Remove %1 Tax").arg(ordinals[i]) aToggle[i]->setText(counter ? tr("&Remove %1 Cast Count").arg(ordinals[i])
: tr("&Add %1 Tax").arg(ordinals[i])); : tr("&Add %1 Cast Count").arg(ordinals[i]));
// Toggle visible if: bool canAdd = !counter && (i == 0 || castCounts[i - 1]);
// - Counter doesn't exist and previous counter is active (can add next in sequence)
// - Counter exists with value 0 and is the highest active (can remove last in sequence)
bool canAdd = !counter && (i == 0 || taxCounters[i - 1]);
bool canRemove = counter && counter->getValue() == 0 && i == highestActive; bool canRemove = counter && counter->getValue() == 0 && i == highestActive;
aToggleTax[i]->setVisible(canAdd || canRemove); aToggle[i]->setVisible(canAdd || canRemove);
} }
} }
@ -144,34 +137,32 @@ void CommandZoneMenu::setShortcutsActive()
{ {
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
// Only first two tax counters have shortcuts if (aIncrease[0]) {
if (aIncreaseTax[0]) { aIncrease[0]->setShortcuts(shortcuts.getShortcut(inc1ShortcutKey));
aIncreaseTax[0]->setShortcuts(shortcuts.getShortcut(incTax1ShortcutKey));
} }
if (aDecreaseTax[0]) { if (aDecrease[0]) {
aDecreaseTax[0]->setShortcuts(shortcuts.getShortcut(decTax1ShortcutKey)); aDecrease[0]->setShortcuts(shortcuts.getShortcut(dec1ShortcutKey));
} }
if (aIncreaseTax[1]) { if (aIncrease[1]) {
aIncreaseTax[1]->setShortcuts(shortcuts.getShortcut(incTax2ShortcutKey)); aIncrease[1]->setShortcuts(shortcuts.getShortcut(inc2ShortcutKey));
} }
if (aDecreaseTax[1]) { if (aDecrease[1]) {
aDecreaseTax[1]->setShortcuts(shortcuts.getShortcut(decTax2ShortcutKey)); aDecrease[1]->setShortcuts(shortcuts.getShortcut(dec2ShortcutKey));
} }
} }
void CommandZoneMenu::setShortcutsInactive() void CommandZoneMenu::setShortcutsInactive()
{ {
// Only first two tax counters have shortcuts if (aIncrease[0]) {
if (aIncreaseTax[0]) { aIncrease[0]->setShortcut(QKeySequence());
aIncreaseTax[0]->setShortcut(QKeySequence());
} }
if (aDecreaseTax[0]) { if (aDecrease[0]) {
aDecreaseTax[0]->setShortcut(QKeySequence()); aDecrease[0]->setShortcut(QKeySequence());
} }
if (aIncreaseTax[1]) { if (aIncrease[1]) {
aIncreaseTax[1]->setShortcut(QKeySequence()); aIncrease[1]->setShortcut(QKeySequence());
} }
if (aDecreaseTax[1]) { if (aDecrease[1]) {
aDecreaseTax[1]->setShortcut(QKeySequence()); aDecrease[1]->setShortcut(QKeySequence());
} }
} }

View file

@ -1,7 +1,7 @@
/** /**
* @file command_zone_menu.h * @file command_zone_menu.h
* @ingroup GameMenusZones * @ingroup GameGraphicsPlayers
* @brief Context menu for command zone right-click actions. * @brief Submenu for command zone actions (cast counts, minimize/restore).
*/ */
#ifndef COCKATRICE_COMMAND_ZONE_MENU_H #ifndef COCKATRICE_COMMAND_ZONE_MENU_H
@ -17,10 +17,10 @@ class PlayerGraphicsItem;
/** /**
* @class CommandZoneMenu * @class CommandZoneMenu
* @brief Context menu for the command zone. * @brief Player submenu for managing cast counts and command zone display.
* *
* Appears when right-clicking on the command zone. Provides actions for * Provides actions to increment/decrement each cast count, toggle cast counts
* adjusting tax counters (up to 5) and toggling minimized state. * on/off, and minimize/restore the command zone display.
* *
* @see PlayerMenu * @see PlayerMenu
* @see CommandZone * @see CommandZone
@ -36,15 +36,15 @@ public:
void setShortcutsInactive() override; void setShortcutsInactive() override;
private: private:
static constexpr int TaxCounterCount = CounterIds::TaxCounterCount; static constexpr int CastCountCount = CastCountIds::Count;
std::array<QAction *, TaxCounterCount> aIncreaseTax{}; std::array<QAction *, CastCountCount> aIncrease{};
std::array<QAction *, TaxCounterCount> aDecreaseTax{}; std::array<QAction *, CastCountCount> aDecrease{};
std::array<QAction *, TaxCounterCount> aToggleTax{}; std::array<QAction *, CastCountCount> aToggle{};
QAction *aToggleMinimized = nullptr; QAction *aToggleMinimized = nullptr;
public slots: public slots:
void updateTaxCounterActionStates(); void updateCastCountActionStates();
private slots: private slots:
void actToggleMinimized(); void actToggleMinimized();
@ -52,11 +52,10 @@ private slots:
private: private:
PlayerGraphicsItem *player; PlayerGraphicsItem *player;
// Shortcuts only for first two tax counters QString inc1ShortcutKey;
QString incTax1ShortcutKey; QString dec1ShortcutKey;
QString decTax1ShortcutKey; QString inc2ShortcutKey;
QString incTax2ShortcutKey; QString dec2ShortcutKey;
QString decTax2ShortcutKey;
}; };
#endif // COCKATRICE_COMMAND_ZONE_MENU_H #endif // COCKATRICE_COMMAND_ZONE_MENU_H

View file

@ -3,7 +3,7 @@
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_card_item.h" #include "../board/abstract_card_item.h"
#include "../board/commander_tax_counter.h" #include "../board/cast_count_widget.h"
#include "../board/counter_general.h" #include "../board/counter_general.h"
#include "../hand_counter.h" #include "../hand_counter.h"
#include "../z_values.h" #include "../z_values.h"
@ -32,6 +32,8 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded); connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved); connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
connect(player, &PlayerLogic::castCountAdded, this, &PlayerGraphicsItem::onCastCountAdded);
connect(player, &PlayerLogic::castCountRemoved, this, &PlayerGraphicsItem::onCastCountRemoved);
playerMenu = new PlayerMenu(this); playerMenu = new PlayerMenu(this);
@ -231,34 +233,12 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
AbstractCounter *widget; AbstractCounter *widget;
if (state->getName() == "life") { if (state->getName() == "life") {
widget = playerTarget->addCounter(state); widget = playerTarget->addCounter(state);
} else if (CounterIds::isTaxCounter(state->getId())) {
if (!commandZoneGraphicsItem) {
qWarning() << "Cannot create tax counter" << state->getName() << "- command zone not available";
return;
}
// Qt parent (commandZoneGraphicsItem) owns widget; counterWidgets map holds reference
// for lookup; CommandZone::registerTaxCounter connects QObject::destroyed for cleanup
widget = new CommanderTaxCounter(state, player, commandZoneGraphicsItem);
widget->setActive(state->isActive());
commandZoneGraphicsItem->registerTaxCounter(widget);
if (auto *menu = playerMenu->getCommandZoneMenu()) {
connect(state, &CounterState::activeChanged, menu, &CommandZoneMenu::updateTaxCounterActionStates);
connect(state, &CounterState::valueChanged, menu, &CommandZoneMenu::updateTaxCounterActionStates);
}
} else { } else {
widget = new GeneralCounter(state, player, true, this); widget = new GeneralCounter(state, player, true, this);
} }
counterWidgets.insert(state->getId(), widget); counterWidgets.insert(state->getId(), widget);
// A counter's submenu follows its isActive() state: while inactive the counter is hidden and
// the server rejects every modification. Only tax counters go inactive today.
setCounterMenuRegistered(widget, state->isActive()); setCounterMenuRegistered(widget, state->isActive());
connect(state, &CounterState::activeChanged, this, [this, counterId = state->getId()](bool newActive) {
if (AbstractCounter *counter = getCounterWidget(counterId)) {
setCounterMenuRegistered(counter, newActive);
}
});
if (playerMenu->getShortcutsActive()) { if (playerMenu->getShortcutsActive()) {
widget->setShortcutsActive(); widget->setShortcutsActive();
@ -267,6 +247,25 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
rearrangeCounters(); rearrangeCounters();
} }
void PlayerGraphicsItem::onCastCountAdded(int index, CounterState *state)
{
if (!commandZoneGraphicsItem) {
qWarning() << "Cannot create cast count" << index << "- command zone not available";
return;
}
auto *widget = new CastCountWidget(state, player, commandZoneGraphicsItem);
commandZoneGraphicsItem->registerCastCount(widget);
castCountWidgets.insert(index, widget);
if (auto *menu = playerMenu->getCommandZoneMenu()) {
menu->updateCastCountActionStates();
connect(state, &CounterState::valueChanged, menu, &CommandZoneMenu::updateCastCountActionStates);
}
rearrangeCounters();
}
void PlayerGraphicsItem::onCounterRemoved(int counterId) void PlayerGraphicsItem::onCounterRemoved(int counterId)
{ {
auto *widget = counterWidgets.take(counterId); auto *widget = counterWidgets.take(counterId);
@ -274,25 +273,37 @@ void PlayerGraphicsItem::onCounterRemoved(int counterId)
return; return;
} }
setCounterMenuRegistered(widget, false); setCounterMenuRegistered(widget, false);
if (commandZoneGraphicsItem && CounterIds::isTaxCounter(widget->getId())) { widget->delCounter();
commandZoneGraphicsItem->unregisterTaxCounter(widget); rearrangeCounters();
}
void PlayerGraphicsItem::onCastCountRemoved(int index)
{
auto *widget = castCountWidgets.take(index);
if (!widget) {
return;
}
if (commandZoneGraphicsItem) {
commandZoneGraphicsItem->unregisterCastCount(widget);
} }
widget->delCounter(); widget->delCounter();
if (auto *menu = playerMenu->getCommandZoneMenu()) {
menu->updateCastCountActionStates();
}
rearrangeCounters(); rearrangeCounters();
} }
void PlayerGraphicsItem::rearrangeCounters() void PlayerGraphicsItem::rearrangeCounters()
{ {
if (commandZoneGraphicsItem) { if (commandZoneGraphicsItem) {
commandZoneGraphicsItem->rearrangeTaxCounters(); commandZoneGraphicsItem->rearrangeCastCounts();
} }
qreal ySize = boundingRect().y() + 80; qreal ySize = boundingRect().y() + 80;
constexpr qreal padding = 5; constexpr qreal padding = 5;
for (auto *ctr : counterWidgets.values()) { for (auto *ctr : counterWidgets.values()) {
if (CounterIds::isTaxCounter(ctr->getId())) {
continue;
}
if (!ctr->getShownInCounterArea()) { if (!ctr->getShownInCounterArea()) {
continue; continue;
} }
@ -302,10 +313,9 @@ void PlayerGraphicsItem::rearrangeCounters()
} }
} }
AbstractCounter *PlayerGraphicsItem::getTaxCounterIfActive(int counterId) const AbstractCounter *PlayerGraphicsItem::getCastCountWidget(int index) const
{ {
AbstractCounter *counter = getCounterWidget(counterId); return castCountWidgets.value(index, nullptr);
return (counter && counter->isActive()) ? counter : nullptr;
} }
void PlayerGraphicsItem::rearrangeZones() void PlayerGraphicsItem::rearrangeZones()

View file

@ -123,14 +123,16 @@ public:
{ {
return counterWidgets.value(counterId, nullptr); return counterWidgets.value(counterId, nullptr);
} }
/** @brief Returns the tax counter if it exists and is active, or nullptr otherwise. */ /** @brief Returns the cast count widget for the given index (1-5), or nullptr if not found. */
[[nodiscard]] AbstractCounter *getTaxCounterIfActive(int counterId) const; [[nodiscard]] AbstractCounter *getCastCountWidget(int index) const;
public slots: public slots:
void onPlayerActiveChanged(bool _active); void onPlayerActiveChanged(bool _active);
void onCustomZoneAdded(QString customZoneName); void onCustomZoneAdded(QString customZoneName);
void onCounterAdded(CounterState *state); void onCounterAdded(CounterState *state);
void onCounterRemoved(int counterId); void onCounterRemoved(int counterId);
void onCastCountAdded(int index, CounterState *state);
void onCastCountRemoved(int index);
void rearrangeCounters(); void rearrangeCounters();
void retranslateUi(); void retranslateUi();
@ -147,6 +149,7 @@ private:
PlayerArea *playerArea; PlayerArea *playerArea;
PlayerTarget *playerTarget; PlayerTarget *playerTarget;
QMap<int, AbstractCounter *> counterWidgets; QMap<int, AbstractCounter *> counterWidgets;
QMap<int, AbstractCounter *> castCountWidgets;
QMap<QString, CardZone *> zoneGraphicsItems; QMap<QString, CardZone *> zoneGraphicsItems;
PileZone *deckZoneGraphicsItem; PileZone *deckZoneGraphicsItem;
PileZone *sideboardGraphicsItem; PileZone *sideboardGraphicsItem;

View file

@ -34,8 +34,8 @@ constexpr qreal OVERLAY_BASE = ZValueLayerManager::OVERLAY_BASE;
// Overlay layer Z-values for items that should appear above normal cards // Overlay layer Z-values for items that should appear above normal cards
constexpr qreal HOVERED_CARD = ZValueLayerManager::overlayZValue(1.0); constexpr qreal HOVERED_CARD = ZValueLayerManager::overlayZValue(1.0);
/** @brief Commander tax counter overlay */ /** @brief Cast count overlay */
constexpr qreal TAX_COUNTERS = ZValueLayerManager::overlayZValue(2.0); constexpr qreal CAST_COUNTS = ZValueLayerManager::overlayZValue(2.0);
constexpr qreal ARROWS = ZValueLayerManager::overlayZValue(3.0); constexpr qreal ARROWS = ZValueLayerManager::overlayZValue(3.0);
constexpr qreal ZONE_VIEW_WIDGET = ZValueLayerManager::overlayZValue(4.0); constexpr qreal ZONE_VIEW_WIDGET = ZValueLayerManager::overlayZValue(4.0);
constexpr qreal DRAG_ITEM = ZValueLayerManager::overlayZValue(5.0); constexpr qreal DRAG_ITEM = ZValueLayerManager::overlayZValue(5.0);

View file

@ -7,7 +7,7 @@
#include "../board/abstract_counter.h" #include "../board/abstract_counter.h"
#include "../board/card_drag_item.h" #include "../board/card_drag_item.h"
#include "../board/card_item.h" #include "../board/card_item.h"
#include "../board/commander_tax_counter.h" #include "../board/cast_count_widget.h"
#include "../z_values.h" #include "../z_values.h"
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
@ -26,7 +26,7 @@ CommandZone::CommandZone(CommandZoneLogic *_logic, int _zoneHeight, QGraphicsIte
CommandZone::~CommandZone() CommandZone::~CommandZone()
{ {
for (AbstractCounter *ctr : taxCounters) { for (AbstractCounter *ctr : castCounts) {
disconnect(ctr, &QObject::destroyed, this, nullptr); disconnect(ctr, &QObject::destroyed, this, nullptr);
} }
} }
@ -52,7 +52,7 @@ void CommandZone::setMinimumHeight(int height)
return; return;
} }
// The floor only affects height while minimized; skip relayout if the displayed // The floor only affects height while minimized; skip relayout if the displayed
// height is unchanged (e.g. a tax counter toggled while expanded). // height is unchanged (e.g. a cast count toggled while expanded).
const qreal oldEffectiveHeight = currentHeight(); const qreal oldEffectiveHeight = currentHeight();
minimumHeight = height; minimumHeight = height;
if (qFuzzyCompare(1.0 + currentHeight(), 1.0 + oldEffectiveHeight)) { if (qFuzzyCompare(1.0 + currentHeight(), 1.0 + oldEffectiveHeight)) {
@ -145,38 +145,32 @@ void CommandZone::reorganizeCards()
update(); update();
} }
void CommandZone::registerTaxCounter(AbstractCounter *counter) void CommandZone::registerCastCount(AbstractCounter *counter)
{ {
if (!counter || taxCounters.contains(counter)) { if (!counter || castCounts.contains(counter)) {
return; return;
} }
taxCounters.append(counter); castCounts.append(counter);
connect(counter, &QObject::destroyed, this, [this, counter]() { taxCounters.removeOne(counter); }); connect(counter, &QObject::destroyed, this, [this, counter]() { castCounts.removeOne(counter); });
} }
void CommandZone::unregisterTaxCounter(AbstractCounter *counter) void CommandZone::unregisterCastCount(AbstractCounter *counter)
{ {
if (!counter) { if (!counter) {
return; return;
} }
disconnect(counter, &QObject::destroyed, this, nullptr); disconnect(counter, &QObject::destroyed, this, nullptr);
taxCounters.removeOne(counter); castCounts.removeOne(counter);
} }
void CommandZone::rearrangeTaxCounters() void CommandZone::rearrangeCastCounts()
{ {
qreal y = TaxCounterSizes::TAX_COUNTER_MARGIN; qreal y = CastCountSizes::MARGIN;
for (AbstractCounter *ctr : taxCounters) { for (AbstractCounter *ctr : castCounts) {
ctr->setPos(TaxCounterSizes::TAX_COUNTER_MARGIN, y); ctr->setPos(CastCountSizes::MARGIN, y);
ctr->setZValue(ZValues::TAX_COUNTERS); ctr->setZValue(ZValues::CAST_COUNTS);
// Visibility is owned solely by AbstractCounter::setActive() (the counter's own flag), y += ctr->getRadius() + CastCountSizes::MARGIN;
// which Qt AND-s with this CommandZone's visibility via child-visibility propagation
// (tax counters are graphics children of the zone). This function only handles layout,
// so it stacks and measures by isActive() alone.
if (ctr->isActive()) {
y += ctr->getRadius() + TaxCounterSizes::TAX_COUNTER_MARGIN;
}
} }
setMinimumHeight(static_cast<int>(y)); setMinimumHeight(static_cast<int>(y));

View file

@ -38,11 +38,11 @@ constexpr qreal COMMAND_ZONE_WIDTH = CardDimensions::WIDTH_F * 1.5;
* *
* Always visible when enabled. Uses the generic vertical stacking layout * Always visible when enabled. Uses the generic vertical stacking layout
* with bottom overflow enabled. Can be minimized via double-click (25% height, * with bottom overflow enabled. Can be minimized via double-click (25% height,
* or the tax-counter floor if higher). * or the cast count floor if higher).
* *
* @see SelectZone::layoutCardsVertically for the stacking algorithm * @see SelectZone::layoutCardsVertically for the stacking algorithm
* @see CommandZoneLogic for card data management * @see CommandZoneLogic for card data management
* @see CommanderTaxCounter for the tax counter overlay * @see CastCountWidget for the cast count overlay
*/ */
class CommandZone : public SelectZone class CommandZone : public SelectZone
{ {
@ -50,9 +50,9 @@ class CommandZone : public SelectZone
private: private:
static constexpr double MINIMIZED_HEIGHT_RATIO = 0.25; static constexpr double MINIMIZED_HEIGHT_RATIO = 0.25;
int zoneHeight; ///< Full height in pixels when expanded int zoneHeight; ///< Full height in pixels when expanded
bool minimized = false; ///< Whether zone is collapsed (25% height, or the tax-counter floor) bool minimized = false; ///< Whether zone is collapsed (25% height, or the cast count floor)
int minimumHeight = 0; ///< Floor for minimized height (e.g. to fit tax counters) int minimumHeight = 0; ///< Floor for minimized height (e.g. to fit cast counts)
QList<AbstractCounter *> taxCounters; ///< Registered tax counter widgets QList<AbstractCounter *> castCounts; ///< Registered cast count widgets
public: public:
/** /**
@ -86,24 +86,24 @@ public:
[[nodiscard]] bool isMinimized() const; [[nodiscard]] bool isMinimized() const;
/** @brief Returns the current display height (full or minimized). */ /** @brief Returns the current display height (full or minimized). */
[[nodiscard]] qreal currentHeight() const; [[nodiscard]] qreal currentHeight() const;
/** @brief Sets the minimum height floor, e.g. to ensure tax counters remain visible. */ /** @brief Sets the minimum height floor, e.g. to ensure cast counts remain visible. */
void setMinimumHeight(int height); void setMinimumHeight(int height);
/** @brief Registers a tax counter widget for layout management. */ /** @brief Registers a cast count widget for layout management. */
void registerTaxCounter(AbstractCounter *counter); void registerCastCount(AbstractCounter *counter);
/** /**
* @brief Unregisters a tax counter widget from layout management. * @brief Unregisters a cast count widget from layout management.
* *
* Call on removal: the widget outlives it via deleteLater(), so unregister synchronously * Call on removal: the widget outlives it via deleteLater(), so unregister synchronously
* to keep it out of the next layout pass. * to keep it out of the next layout pass.
*/ */
void unregisterTaxCounter(AbstractCounter *counter); void unregisterCastCount(AbstractCounter *counter);
/** @brief Lays out visible tax counters vertically in the top-left corner of the command zone. */ /** @brief Lays out visible cast counts vertically in the top-left corner of the command zone. */
void rearrangeTaxCounters(); void rearrangeCastCounts();
signals: signals:
/** @brief Emitted when the zone's minimized state changes. */ /** @brief Emitted when the zone's minimized state changes. */
void minimizedChanged(bool isMinimized); void minimizedChanged(bool isMinimized);
/** @brief Emitted when display height changes without a minimized-state change (e.g. tax counter toggled while /** @brief Emitted when display height changes without a minimized-state change (e.g. cast count toggled while
* minimized). */ * minimized). */
void effectiveHeightChanged(); void effectiveHeightChanged();

View file

@ -13,16 +13,19 @@
#include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h> #include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h>
#include <libcockatrice/protocol/pb/command_concede.pb.h> #include <libcockatrice/protocol/pb/command_concede.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h> #include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_create_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_create_counter.pb.h> #include <libcockatrice/protocol/pb/command_create_counter.pb.h>
#include <libcockatrice/protocol/pb/command_create_token.pb.h> #include <libcockatrice/protocol/pb/command_create_token.pb.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h> #include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_del_counter.pb.h> #include <libcockatrice/protocol/pb/command_del_counter.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h> #include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_delete_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h> #include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h> #include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/command_flip_card.pb.h> #include <libcockatrice/protocol/pb/command_flip_card.pb.h>
#include <libcockatrice/protocol/pb/command_game_say.pb.h> #include <libcockatrice/protocol/pb/command_game_say.pb.h>
#include <libcockatrice/protocol/pb/command_inc_card_counter.pb.h> #include <libcockatrice/protocol/pb/command_inc_card_counter.pb.h>
#include <libcockatrice/protocol/pb/command_inc_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h> #include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
#include <libcockatrice/protocol/pb/command_kick_from_game.pb.h> #include <libcockatrice/protocol/pb/command_kick_from_game.pb.h>
#include <libcockatrice/protocol/pb/command_leave_game.pb.h> #include <libcockatrice/protocol/pb/command_leave_game.pb.h>
@ -36,8 +39,8 @@
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h> #include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.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_card_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_set_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_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
@ -343,7 +346,28 @@ Response::ResponseCode Server_AbstractParticipant::cmdDelCounter(const Command_D
return Response::RespFunctionNotAllowed; return Response::RespFunctionNotAllowed;
} }
Response::ResponseCode Server_AbstractParticipant::cmdSetCounterActive(const Command_SetCounterActive & /*cmd*/, Response::ResponseCode Server_AbstractParticipant::cmdCreateCastCount(const Command_CreateCastCount & /*cmd*/,
ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/)
{
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode Server_AbstractParticipant::cmdDeleteCastCount(const Command_DeleteCastCount & /*cmd*/,
ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/)
{
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode Server_AbstractParticipant::cmdIncCastCount(const Command_IncCastCount & /*cmd*/,
ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/)
{
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode Server_AbstractParticipant::cmdSetCastCount(const Command_SetCastCount & /*cmd*/,
ResponseContainer & /*rc*/, ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/) GameEventStorage & /*ges*/)
{ {
@ -533,8 +557,17 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
case GameCommand::REVERSE_TURN: case GameCommand::REVERSE_TURN:
return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges); return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges);
break; break;
case GameCommand::SET_COUNTER_ACTIVE: case GameCommand::CREATE_CAST_COUNT:
return cmdSetCounterActive(command.GetExtension(Command_SetCounterActive::ext), rc, ges); return cmdCreateCastCount(command.GetExtension(Command_CreateCastCount::ext), rc, ges);
break;
case GameCommand::DELETE_CAST_COUNT:
return cmdDeleteCastCount(command.GetExtension(Command_DeleteCastCount::ext), rc, ges);
break;
case GameCommand::INC_CAST_COUNT:
return cmdIncCastCount(command.GetExtension(Command_IncCastCount::ext), rc, ges);
break;
case GameCommand::SET_CAST_COUNT:
return cmdSetCastCount(command.GetExtension(Command_SetCastCount::ext), rc, ges);
break; break;
default: default:
return Response::RespInvalidCommand; return Response::RespInvalidCommand;

View file

@ -41,7 +41,6 @@ class Command_Judge;
class Command_IncCounter; class Command_IncCounter;
class Command_CreateCounter; class Command_CreateCounter;
class Command_SetCounter; class Command_SetCounter;
class Command_SetCounterActive;
class Command_DelCounter; class Command_DelCounter;
class Command_NextTurn; class Command_NextTurn;
class Command_SetActivePhase; class Command_SetActivePhase;
@ -53,6 +52,10 @@ class Command_SetSideboardPlan;
class Command_DeckSelect; class Command_DeckSelect;
class Command_SetSideboardLock; class Command_SetSideboardLock;
class Command_ChangeZoneProperties; class Command_ChangeZoneProperties;
class Command_CreateCastCount;
class Command_DeleteCastCount;
class Command_IncCastCount;
class Command_SetCastCount;
class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container
{ {
@ -162,8 +165,6 @@ public:
virtual Response::ResponseCode virtual Response::ResponseCode
cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode virtual Response::ResponseCode
cmdSetCounterActive(const Command_SetCounterActive &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges); cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode virtual Response::ResponseCode
cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges); cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges);
@ -175,6 +176,14 @@ public:
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges);
virtual Response::ResponseCode virtual Response::ResponseCode
cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges); cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdCreateCastCount(const Command_CreateCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdDeleteCastCount(const Command_DeleteCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdIncCastCount(const Command_IncCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges);
virtual Response::ResponseCode
cmdSetCastCount(const Command_SetCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges);
Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges);
void sendGameEvent(const GameEventContainer &event); void sendGameEvent(const GameEventContainer &event);

View file

@ -18,33 +18,39 @@
#include <libcockatrice/protocol/pb/command_attach_card.pb.h> #include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h> #include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h>
#include <libcockatrice/protocol/pb/command_concede.pb.h> #include <libcockatrice/protocol/pb/command_concede.pb.h>
#include <libcockatrice/protocol/pb/command_create_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_create_counter.pb.h> #include <libcockatrice/protocol/pb/command_create_counter.pb.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h> #include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_del_counter.pb.h> #include <libcockatrice/protocol/pb/command_del_counter.pb.h>
#include <libcockatrice/protocol/pb/command_delete_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h> #include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_inc_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h> #include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h> #include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/command_mulligan.pb.h> #include <libcockatrice/protocol/pb/command_mulligan.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h> #include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_cast_count.pb.h>
#include <libcockatrice/protocol/pb/command_set_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_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h> #include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/context_deck_select.pb.h> #include <libcockatrice/protocol/pb/context_deck_select.pb.h>
#include <libcockatrice/protocol/pb/context_mulligan.pb.h> #include <libcockatrice/protocol/pb/context_mulligan.pb.h>
#include <libcockatrice/protocol/pb/context_set_sideboard_lock.pb.h> #include <libcockatrice/protocol/pb/context_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/event_create_cast_count.pb.h>
#include <libcockatrice/protocol/pb/event_create_counter.pb.h> #include <libcockatrice/protocol/pb/event_create_counter.pb.h>
#include <libcockatrice/protocol/pb/event_del_counter.pb.h> #include <libcockatrice/protocol/pb/event_del_counter.pb.h>
#include <libcockatrice/protocol/pb/event_delete_cast_count.pb.h>
#include <libcockatrice/protocol/pb/event_draw_cards.pb.h> #include <libcockatrice/protocol/pb/event_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/event_game_log_notice.pb.h> #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_player_properties_changed.pb.h>
#include <libcockatrice/protocol/pb/event_set_cast_count.pb.h>
#include <libcockatrice/protocol/pb/event_set_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/protocol/pb/event_shuffle.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h> #include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pb/response_dump_zone.pb.h> #include <libcockatrice/protocol/pb/response_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_cast_count.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h> #include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h> #include <libcockatrice/rng/rng_abstract.h>
@ -75,7 +81,6 @@ int Server_Player::newCounterId() const
id = c->getId(); id = c->getId();
} }
} }
// Skip reserved IDs 0-9 even in non-Commander games to prevent collision
return std::max(id + 1, CounterIds::FirstUserId); return std::max(id + 1, CounterIds::FirstUserId);
} }
@ -109,15 +114,7 @@ void Server_Player::setupZones()
// Command zone for Commander format // Command zone for Commander format
if (game->getEnableCommandZone()) { if (game->getEnableCommandZone()) {
addZone(new Server_CardZone(this, ZoneNames::COMMAND, false, ServerInfo_Zone::PublicZone)); addZone(new Server_CardZone(this, ZoneNames::COMMAND, false, ServerInfo_Zone::PublicZone));
for (int i = 0; i < CounterIds::TaxCounterCount; ++i) { castCounts.insert(1, 0);
int id = CounterIds::taxCounterIdFromIndex(i);
const char *name = CounterNames::forId(id);
auto *counter = new Server_Counter(id, name, makeColor(128, 128, 128), 20, 0, 0, MAX_COUNTER_VALUE);
if (i > 0) {
(void)counter->setActive(false);
}
addCounter(counter);
}
} }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@ -174,6 +171,7 @@ void Server_Player::clearZones()
delete counter; delete counter;
} }
counters.clear(); counters.clear();
castCounts.clear();
lastDrawList.clear(); lastDrawList.clear();
} }
@ -445,11 +443,8 @@ Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer &
return retVal; return retVal;
} }
Response::ResponseCode Server_Player::evaluateModifyCounter(bool gameStarted, Response::ResponseCode
bool playerConceded, Server_Player::evaluateModifyCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter)
bool commandZoneEnabled,
int counterId,
const Server_Counter *counter)
{ {
if (!gameStarted) { if (!gameStarted) {
return Response::RespGameNotStarted; return Response::RespGameNotStarted;
@ -457,17 +452,6 @@ Response::ResponseCode Server_Player::evaluateModifyCounter(bool gameStarted,
if (playerConceded) { if (playerConceded) {
return Response::RespContextError; return Response::RespContextError;
} }
if (CounterIds::isTaxCounter(counterId)) {
// Tax counters are server-managed: they only exist in Commander games, and an
// inactive (hidden) tax counter must stay at zero. Block modification in either
// case so the value can never diverge from what players can see.
if (!commandZoneEnabled) {
return Response::RespContextError;
}
if (counter && !counter->isActive()) {
return Response::RespContextError;
}
}
if (!counter) { if (!counter) {
return Response::RespNameNotFound; return Response::RespNameNotFound;
} }
@ -480,8 +464,7 @@ Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &
const int counterId = cmd.counter_id(); const int counterId = cmd.counter_id();
Server_Counter *c = counters.value(counterId, nullptr); Server_Counter *c = counters.value(counterId, nullptr);
const Response::ResponseCode authResult = const Response::ResponseCode authResult = evaluateModifyCounter(game->getGameStarted(), conceded, c);
evaluateModifyCounter(game->getGameStarted(), conceded, game->getEnableCommandZone(), counterId, c);
if (authResult != Response::RespOk) { if (authResult != Response::RespOk) {
return authResult; return authResult;
} }
@ -498,7 +481,7 @@ Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &
} }
Response::ResponseCode Response::ResponseCode
Server_Player::evaluateCreateCounter(bool gameStarted, bool playerConceded, const QString &counterName) Server_Player::evaluateCreateCounter(bool gameStarted, bool playerConceded, const QString & /*counterName*/)
{ {
if (!gameStarted) { if (!gameStarted) {
return Response::RespGameNotStarted; return Response::RespGameNotStarted;
@ -506,12 +489,6 @@ Server_Player::evaluateCreateCounter(bool gameStarted, bool playerConceded, cons
if (playerConceded) { if (playerConceded) {
return Response::RespContextError; return Response::RespContextError;
} }
// Reserved system counter names (commander/partner tax) are how clients identify
// server-managed tax counters for rendering and logging; a client must not be able
// to spoof one via a user-created counter.
if (CounterNames::isTaxCounter(counterName)) {
return Response::RespFunctionNotAllowed;
}
return Response::RespOk; return Response::RespOk;
} }
@ -546,8 +523,7 @@ Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &
const int counterId = cmd.counter_id(); const int counterId = cmd.counter_id();
Server_Counter *c = counters.value(counterId, nullptr); Server_Counter *c = counters.value(counterId, nullptr);
const Response::ResponseCode authResult = const Response::ResponseCode authResult = evaluateModifyCounter(game->getGameStarted(), conceded, c);
evaluateModifyCounter(game->getGameStarted(), conceded, game->getEnableCommandZone(), counterId, c);
if (authResult != Response::RespOk) { if (authResult != Response::RespOk) {
return authResult; return authResult;
} }
@ -564,7 +540,7 @@ Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &
} }
Response::ResponseCode Response::ResponseCode
Server_Player::evaluateDelCounter(bool gameStarted, bool playerConceded, int counterId, const Server_Counter *counter) Server_Player::evaluateDelCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter)
{ {
if (!gameStarted) { if (!gameStarted) {
return Response::RespGameNotStarted; return Response::RespGameNotStarted;
@ -572,12 +548,6 @@ Server_Player::evaluateDelCounter(bool gameStarted, bool playerConceded, int cou
if (playerConceded) { if (playerConceded) {
return Response::RespContextError; return Response::RespContextError;
} }
// Reserved tax counters are server-managed system counters and must never be
// deleted by a client. When the command zone is disabled they don't exist, so
// a lookup would fail anyway; when it's enabled they must persist for the game.
if (CounterIds::isTaxCounter(counterId)) {
return Response::RespFunctionNotAllowed;
}
if (!counter) { if (!counter) {
return Response::RespNameNotFound; return Response::RespNameNotFound;
} }
@ -590,7 +560,7 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &
const int counterId = cmd.counter_id(); const int counterId = cmd.counter_id();
Server_Counter *counter = counters.value(counterId, nullptr); Server_Counter *counter = counters.value(counterId, nullptr);
const Response::ResponseCode authResult = evaluateDelCounter(game->getGameStarted(), conceded, counterId, counter); const Response::ResponseCode authResult = evaluateDelCounter(game->getGameStarted(), conceded, counter);
if (authResult != Response::RespOk) { if (authResult != Response::RespOk) {
return authResult; return authResult;
} }
@ -605,14 +575,14 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &
return Response::RespOk; return Response::RespOk;
} }
Response::ResponseCode Server_Player::evaluateSetCounterActive(bool gameStarted, // Cast count validation and command handlers
Response::ResponseCode Server_Player::evaluateCreateCastCount(bool gameStarted,
bool playerConceded, bool playerConceded,
bool commandZoneEnabled, bool commandZoneEnabled,
int counterId, int index,
const Server_Counter *counter, bool exists,
bool requestedActive, bool predecessorExists)
const Server_Counter *predecessorCounter,
const Server_Counter *successorCounter)
{ {
if (!gameStarted) { if (!gameStarted) {
return Response::RespGameNotStarted; return Response::RespGameNotStarted;
@ -620,53 +590,166 @@ Response::ResponseCode Server_Player::evaluateSetCounterActive(bool gameStarted,
if (playerConceded) { if (playerConceded) {
return Response::RespContextError; return Response::RespContextError;
} }
if (!CounterIds::isTaxCounter(counterId)) {
return Response::RespFunctionNotAllowed;
}
if (!commandZoneEnabled) { if (!commandZoneEnabled) {
return Response::RespContextError; return Response::RespContextError;
} }
if (!counter) { if (!CastCountIds::isValidIndex(index)) {
return Response::RespNameNotFound;
}
// Prevent disabling a counter with tax accumulated; player must reset to 0 first
if (!requestedActive && counter->getCount() != 0) {
return Response::RespContextError; return Response::RespContextError;
} }
// Enforce ordering: can only activate if predecessor is active if (exists) {
if (requestedActive && predecessorCounter && !predecessorCounter->isActive()) {
return Response::RespContextError; return Response::RespContextError;
} }
// Enforce ordering: can only deactivate if successor is inactive if (index > 1 && !predecessorExists) {
if (!requestedActive && successorCounter && successorCounter->isActive()) {
return Response::RespContextError; return Response::RespContextError;
} }
return Response::RespOk; return Response::RespOk;
} }
Response::ResponseCode Server_Player::cmdSetCounterActive(const Command_SetCounterActive &cmd, Response::ResponseCode Server_Player::evaluateDeleteCastCount(bool gameStarted,
ResponseContainer & /*rc*/, bool playerConceded,
GameEventStorage &ges) bool commandZoneEnabled,
int index,
bool exists,
int value,
bool successorExists)
{ {
const int counterId = cmd.counter_id(); if (!gameStarted) {
Server_Counter *c = counters.value(counterId, nullptr); return Response::RespGameNotStarted;
}
if (playerConceded) {
return Response::RespContextError;
}
if (!commandZoneEnabled) {
return Response::RespContextError;
}
if (!CastCountIds::isValidIndex(index)) {
return Response::RespContextError;
}
if (!exists) {
return Response::RespNameNotFound;
}
if (value != 0) {
return Response::RespContextError;
}
if (successorExists) {
return Response::RespContextError;
}
return Response::RespOk;
}
int predecessorId = CounterIds::taxCounterIdFromIndex(CounterIds::taxCounterIndex(counterId) - 1); Response::ResponseCode Server_Player::evaluateModifyCastCount(bool gameStarted,
int successorId = CounterIds::taxCounterIdFromIndex(CounterIds::taxCounterIndex(counterId) + 1); bool playerConceded,
Server_Counter *predecessor = predecessorId >= 0 ? counters.value(predecessorId, nullptr) : nullptr; bool commandZoneEnabled,
Server_Counter *successor = successorId >= 0 ? counters.value(successorId, nullptr) : nullptr; int index,
bool exists)
{
if (!gameStarted) {
return Response::RespGameNotStarted;
}
if (playerConceded) {
return Response::RespContextError;
}
if (!commandZoneEnabled) {
return Response::RespContextError;
}
if (!CastCountIds::isValidIndex(index)) {
return Response::RespContextError;
}
if (!exists) {
return Response::RespNameNotFound;
}
return Response::RespOk;
}
const Response::ResponseCode authResult = Response::ResponseCode
evaluateSetCounterActive(game->getGameStarted(), conceded, game->getEnableCommandZone(), counterId, c, Server_Player::cmdCreateCastCount(const Command_CreateCastCount &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
cmd.active(), predecessor, successor); {
const int index = cmd.index();
bool exists = castCounts.contains(index);
bool predecessorExists = (index == 1) || castCounts.contains(index - 1);
const Response::ResponseCode authResult = evaluateCreateCastCount(
game->getGameStarted(), conceded, game->getEnableCommandZone(), index, exists, predecessorExists);
if (authResult != Response::RespOk) { if (authResult != Response::RespOk) {
return authResult; return authResult;
} }
if (c->setActive(cmd.active())) { castCounts.insert(index, 0);
Event_SetCounterActive event;
event.set_counter_id(c->getId()); Event_CreateCastCount event;
event.set_active(c->isActive()); event.set_index(index);
ges.enqueueGameEvent(event, playerId);
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdDeleteCastCount(const Command_DeleteCastCount &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
const int index = cmd.index();
bool exists = castCounts.contains(index);
int value = exists ? castCounts.value(index) : 0;
bool successorExists = castCounts.contains(index + 1);
const Response::ResponseCode authResult = evaluateDeleteCastCount(
game->getGameStarted(), conceded, game->getEnableCommandZone(), index, exists, value, successorExists);
if (authResult != Response::RespOk) {
return authResult;
}
castCounts.remove(index);
Event_DeleteCastCount event;
event.set_index(index);
ges.enqueueGameEvent(event, playerId);
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdIncCastCount(const Command_IncCastCount &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
const int index = cmd.index();
bool exists = castCounts.contains(index);
const Response::ResponseCode authResult =
evaluateModifyCastCount(game->getGameStarted(), conceded, game->getEnableCommandZone(), index, exists);
if (authResult != Response::RespOk) {
return authResult;
}
int oldValue = castCounts.value(index);
int newValue = qBound(0, oldValue + cmd.delta(), MAX_COUNTER_VALUE);
if (newValue != oldValue) {
castCounts.insert(index, newValue);
Event_SetCastCount event;
event.set_index(index);
event.set_value(newValue);
ges.enqueueGameEvent(event, playerId);
}
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdSetCastCount(const Command_SetCastCount &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
const int index = cmd.index();
bool exists = castCounts.contains(index);
const Response::ResponseCode authResult =
evaluateModifyCastCount(game->getGameStarted(), conceded, game->getEnableCommandZone(), index, exists);
if (authResult != Response::RespOk) {
return authResult;
}
int oldValue = castCounts.value(index);
int newValue = qBound(0, cmd.value(), MAX_COUNTER_VALUE);
if (newValue != oldValue) {
castCounts.insert(index, newValue);
Event_SetCastCount event;
event.set_index(index);
event.set_value(newValue);
ges.enqueueGameEvent(event, playerId); ges.enqueueGameEvent(event, playerId);
} }
@ -746,4 +829,10 @@ void Server_Player::getInfo(ServerInfo_Player *info,
for (Server_Counter *counter : counters) { for (Server_Counter *counter : counters) {
counter->getInfo(info->add_counter_list()); counter->getInfo(info->add_counter_list());
} }
for (auto it = castCounts.constBegin(); it != castCounts.constEnd(); ++it) {
auto *castCount = info->add_cast_count_list();
castCount->set_index(it.key());
castCount->set_value(it.value());
}
} }

View file

@ -8,6 +8,7 @@ class Server_Player : public Server_AbstractPlayer
Q_OBJECT Q_OBJECT
private: private:
QMap<int, Server_Counter *> counters; QMap<int, Server_Counter *> counters;
QMap<int, int> castCounts; // index (1-5) -> value
QList<int> lastDrawList; QList<int> lastDrawList;
public: public:
@ -21,90 +22,40 @@ public:
{ {
return counters; return counters;
} }
const QMap<int, int> &getCastCounts() const
{
return castCounts;
}
int newCounterId() const; int newCounterId() const;
void addCounter(Server_Counter *counter); void addCounter(Server_Counter *counter);
/** @name Counter command authorization
* Decision logic extracted from the corresponding cmd* handlers so it can be unit-tested
* in isolation. Each function takes all relevant state as parameters and touches no
* instance members, hence static.
* @{
*/
/**
* @brief Decide whether a client may delete a counter.
*
* Reserved tax counters are server-managed and may never be deleted by a client.
*
* @param gameStarted Whether the game has started
* @param playerConceded Whether the player has conceded
* @param counterId ID of the counter to delete
* @param counter Counter with id counterId, or nullptr if the player has no such counter.
* @return Response::RespOk if permitted, otherwise the error response for the client.
*/
static Response::ResponseCode static Response::ResponseCode
evaluateDelCounter(bool gameStarted, bool playerConceded, int counterId, const Server_Counter *counter); evaluateDelCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter);
/** static Response::ResponseCode
* @brief Decide whether a client may change a counter's active (visible) state. evaluateModifyCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter);
*
* Only reserved tax counters can be toggled, and one holding a non-zero value must be reset
* to zero before it can be deactivated.
*
* @param gameStarted Whether the game has started
* @param playerConceded Whether the player has conceded
* @param commandZoneEnabled Whether command zone is enabled for this game
* @param counterId ID of the counter to toggle
* @param counter Counter with id counterId, or nullptr if the player has no such counter.
* @param requestedActive Active state the client asked for.
* @param predecessorCounter Tax counter that must be active before this one can be activated (nullptr if none).
* @param successorCounter Tax counter that must be inactive before this one can be deactivated (nullptr if none).
* @return Response::RespOk if permitted, otherwise the error response for the client.
*/
static Response::ResponseCode evaluateSetCounterActive(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
int counterId,
const Server_Counter *counter,
bool requestedActive,
const Server_Counter *predecessorCounter,
const Server_Counter *successorCounter);
/**
* @brief Decide whether a client may change a counter's value.
*
* Shared by cmdIncCounter and cmdSetCounter. Reserved tax counters may only be modified
* inside a Commander game and only while active, so an inactive (hidden) tax counter can
* never accumulate a value behind the scenes.
*
* @param gameStarted Whether the game has started
* @param playerConceded Whether the player has conceded
* @param commandZoneEnabled Whether command zone is enabled for this game
* @param counterId ID of the counter to modify
* @param counter Counter with id counterId, or nullptr if the player has no such counter.
* @return Response::RespOk if permitted, otherwise the error response for the client.
*/
static Response::ResponseCode evaluateModifyCounter(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
int counterId,
const Server_Counter *counter);
/**
* @brief Decide whether a client may create a counter with the given name.
*
* Reserved system counter names (commander/partner tax) are rejected to prevent
* clients from spoofing server-managed tax counters.
*
* @param gameStarted Whether the game has started
* @param playerConceded Whether the player has conceded
* @param counterName Name requested for the new counter
* @return Response::RespOk if permitted, otherwise the error response for the client.
*/
static Response::ResponseCode static Response::ResponseCode
evaluateCreateCounter(bool gameStarted, bool playerConceded, const QString &counterName); evaluateCreateCounter(bool gameStarted, bool playerConceded, const QString &counterName);
/** @} */ // Cast count validation
static Response::ResponseCode evaluateCreateCastCount(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
int index,
bool exists,
bool predecessorExists);
static Response::ResponseCode evaluateDeleteCastCount(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
int index,
bool exists,
int value,
bool successorExists);
static Response::ResponseCode
evaluateModifyCastCount(bool gameStarted, bool playerConceded, bool commandZoneEnabled, int index, bool exists);
void setupZones() override; void setupZones() override;
void clearZones() override; void clearZones() override;
@ -139,8 +90,6 @@ public:
Response::ResponseCode Response::ResponseCode
cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode Response::ResponseCode
cmdSetCounterActive(const Command_SetCounterActive &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode
cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) override; cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode Response::ResponseCode
cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override; cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
@ -150,6 +99,16 @@ public:
ResponseContainer &rc, ResponseContainer &rc,
GameEventStorage &ges) override; GameEventStorage &ges) override;
// Cast count commands
Response::ResponseCode
cmdCreateCastCount(const Command_CreateCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode
cmdDeleteCastCount(const Command_DeleteCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode
cmdIncCastCount(const Command_IncCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
Response::ResponseCode
cmdSetCastCount(const Command_SetCastCount &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
void getInfo(ServerInfo_Player *info, void getInfo(ServerInfo_Player *info,
Server_AbstractParticipant *playerWhosAsking, Server_AbstractParticipant *playerWhosAsking,
bool omniscient, bool omniscient,

View file

@ -10,6 +10,7 @@ set(PROTO_FILES
command_change_zone_properties.proto command_change_zone_properties.proto
command_concede.proto command_concede.proto
command_create_arrow.proto command_create_arrow.proto
command_create_cast_count.proto
command_create_counter.proto command_create_counter.proto
command_create_token.proto command_create_token.proto
command_deck_del.proto command_deck_del.proto
@ -21,11 +22,13 @@ set(PROTO_FILES
command_deck_upload.proto command_deck_upload.proto
command_del_counter.proto command_del_counter.proto
command_delete_arrow.proto command_delete_arrow.proto
command_delete_cast_count.proto
command_draw_cards.proto command_draw_cards.proto
command_dump_zone.proto command_dump_zone.proto
command_flip_card.proto command_flip_card.proto
command_game_say.proto command_game_say.proto
command_inc_card_counter.proto command_inc_card_counter.proto
command_inc_cast_count.proto
command_inc_counter.proto command_inc_counter.proto
command_kick_from_game.proto command_kick_from_game.proto
command_leave_game.proto command_leave_game.proto
@ -45,8 +48,8 @@ set(PROTO_FILES
command_set_active_phase.proto command_set_active_phase.proto
command_set_card_attr.proto command_set_card_attr.proto
command_set_card_counter.proto command_set_card_counter.proto
command_set_cast_count.proto
command_set_counter.proto command_set_counter.proto
command_set_counter_active.proto
command_set_sideboard_lock.proto command_set_sideboard_lock.proto
command_set_sideboard_plan.proto command_set_sideboard_plan.proto
command_shuffle.proto command_shuffle.proto
@ -66,10 +69,12 @@ set(PROTO_FILES
event_change_zone_properties.proto event_change_zone_properties.proto
event_connection_closed.proto event_connection_closed.proto
event_create_arrow.proto event_create_arrow.proto
event_create_cast_count.proto
event_create_counter.proto event_create_counter.proto
event_create_token.proto event_create_token.proto
event_del_counter.proto event_del_counter.proto
event_delete_arrow.proto event_delete_arrow.proto
event_delete_cast_count.proto
event_destroy_card.proto event_destroy_card.proto
event_draw_cards.proto event_draw_cards.proto
event_dump_zone.proto event_dump_zone.proto
@ -106,8 +111,8 @@ set(PROTO_FILES
event_set_active_player.proto event_set_active_player.proto
event_set_card_attr.proto event_set_card_attr.proto
event_set_card_counter.proto event_set_card_counter.proto
event_set_cast_count.proto
event_set_counter.proto event_set_counter.proto
event_set_counter_active.proto
event_shuffle.proto event_shuffle.proto
event_user_joined.proto event_user_joined.proto
event_user_left.proto event_user_left.proto
@ -149,6 +154,7 @@ set(PROTO_FILES
server_message.proto server_message.proto
serverinfo_arrow.proto serverinfo_arrow.proto
serverinfo_ban.proto serverinfo_ban.proto
serverinfo_cast_count.proto
serverinfo_card.proto serverinfo_card.proto
serverinfo_cardcounter.proto serverinfo_cardcounter.proto
serverinfo_chat_message.proto serverinfo_chat_message.proto

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "game_commands.proto";
message Command_CreateCastCount {
extend GameCommand {
optional Command_CreateCastCount ext = 1036;
}
optional sint32 index = 1 [default = 1];
}

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "game_commands.proto";
message Command_DeleteCastCount {
extend GameCommand {
optional Command_DeleteCastCount ext = 1037;
}
optional sint32 index = 1 [default = 1];
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
import "game_commands.proto";
message Command_IncCastCount {
extend GameCommand {
optional Command_IncCastCount ext = 1038;
}
optional sint32 index = 1 [default = 1];
optional sint32 delta = 2 [default = 1];
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
import "game_commands.proto";
message Command_SetCastCount {
extend GameCommand {
optional Command_SetCastCount ext = 1039;
}
optional sint32 index = 1 [default = 1];
optional sint32 value = 2 [default = 0];
}

View file

@ -1,9 +0,0 @@
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,9 @@
syntax = "proto2";
import "game_event.proto";
message Event_CreateCastCount {
extend GameEvent {
optional Event_CreateCastCount ext = 2024;
}
optional sint32 index = 1 [default = 1];
}

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "game_event.proto";
message Event_DeleteCastCount {
extend GameEvent {
optional Event_DeleteCastCount ext = 2025;
}
optional sint32 index = 1 [default = 1];
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
import "game_event.proto";
message Event_SetCastCount {
extend GameEvent {
optional Event_SetCastCount ext = 2026;
}
optional sint32 index = 1 [default = 1];
optional sint32 value = 2 [default = 0];
}

View file

@ -1,10 +0,0 @@
syntax = "proto2";
import "game_event.proto";
message Event_SetCounterActive {
extend GameEvent {
optional Event_SetCounterActive ext = 2023;
}
optional sint32 counter_id = 1 [default = -1];
optional bool active = 2 [default = true];
}

View file

@ -176,10 +176,25 @@ message GameCommand {
/// Client: reflected via subsequent turn events /// Client: reflected via subsequent turn events
REVERSE_TURN = 1034; REVERSE_TURN = 1034;
/// Show or hide a reserved tax counter without deleting it. /// Create a cast count at the specified index.
/// Server: Server_Player::cmdSetCounterActive /// Server: Server_Player::cmdCreateCastCount
/// Client: PlayerEventHandler::eventSetCounterActive /// Client: PlayerEventHandler::eventCreateCastCount
SET_COUNTER_ACTIVE = 1035; CREATE_CAST_COUNT = 1036;
/// Delete a cast count at the specified index.
/// Server: Server_Player::cmdDeleteCastCount
/// Client: PlayerEventHandler::eventDeleteCastCount
DELETE_CAST_COUNT = 1037;
/// Increment or decrement a cast count.
/// Server: Server_Player::cmdIncCastCount
/// Client: PlayerEventHandler::eventSetCastCount
INC_CAST_COUNT = 1038;
/// Set a cast count to a specific value.
/// Server: Server_Player::cmdSetCastCount
/// Client: PlayerEventHandler::eventSetCastCount
SET_CAST_COUNT = 1039;
} }
extensions 100 to max; extensions 100 to max;

View file

@ -34,7 +34,9 @@ message GameEvent {
CHANGE_ZONE_PROPERTIES = 2020; CHANGE_ZONE_PROPERTIES = 2020;
REVERSE_TURN = 2021; REVERSE_TURN = 2021;
GAME_LOG_NOTICE = 2022; GAME_LOG_NOTICE = 2022;
SET_COUNTER_ACTIVE = 2023; CREATE_CAST_COUNT = 2024;
DELETE_CAST_COUNT = 2025;
SET_CAST_COUNT = 2026;
} }
optional sint32 player_id = 1 [default = -1]; optional sint32 player_id = 1 [default = -1];
extensions 100 to max; extensions 100 to max;

View file

@ -0,0 +1,6 @@
syntax = "proto2";
message ServerInfo_CastCount {
optional sint32 index = 1;
optional sint32 value = 2;
}

View file

@ -3,6 +3,7 @@ import "serverinfo_zone.proto";
import "serverinfo_counter.proto"; import "serverinfo_counter.proto";
import "serverinfo_arrow.proto"; import "serverinfo_arrow.proto";
import "serverinfo_playerproperties.proto"; import "serverinfo_playerproperties.proto";
import "serverinfo_cast_count.proto";
message ServerInfo_Player { message ServerInfo_Player {
optional ServerInfo_PlayerProperties properties = 1; optional ServerInfo_PlayerProperties properties = 1;
@ -10,4 +11,5 @@ message ServerInfo_Player {
repeated ServerInfo_Zone zone_list = 3; repeated ServerInfo_Zone zone_list = 3;
repeated ServerInfo_Counter counter_list = 4; repeated ServerInfo_Counter counter_list = 4;
repeated ServerInfo_Arrow arrow_list = 5; repeated ServerInfo_Arrow arrow_list = 5;
repeated ServerInfo_CastCount cast_count_list = 6;
} }

View file

@ -1,7 +1,10 @@
/** /**
* @file counter_ids.h * @file counter_ids.h
* @ingroup Core * @ingroup Utility
* @brief Shared counter IDs and names for system counters (e.g. tax counters). * @brief Counter ID ranges and cast count index utilities.
*
* Defines the boundary between built-in counters (life, mana colors, storm) and
* user-created counters, plus helpers for the cast count system used in Commander.
*/ */
#ifndef COCKATRICE_COUNTER_IDS_H #ifndef COCKATRICE_COUNTER_IDS_H
@ -11,95 +14,36 @@
/** /**
* @namespace CounterIds * @namespace CounterIds
* @brief Shared counter IDs used by both client and server. * @brief Reserved counter ID ranges.
*
* Single source of truth: included directly by both sides, so they cannot drift.
*
* Reserved counter IDs for system counters:
* IDs 0-7: Standard player counters (life, mana colors, storm)
* IDs 8-12: Tax counters (1st through 5th)
* IDs 13+: Available for user-created counters (FirstUserId)
*
* newCounterId() always returns >= FirstUserId to prevent user counters
* from colliding with reserved IDs, even in non-Commander games.
*/ */
namespace CounterIds namespace CounterIds
{ {
constexpr int TaxCounter1 = 8; ///< 1st tax counter constexpr int FirstUserId = 8; ///< First ID available for user-created counters (0-7 are built-in)
constexpr int TaxCounter2 = 9; ///< 2nd tax counter
constexpr int TaxCounter3 = 10; ///< 3rd tax counter
constexpr int TaxCounter4 = 11; ///< 4th tax counter
constexpr int TaxCounter5 = 12; ///< 5th tax counter
constexpr int FirstUserId = 13; ///< First ID available for user-created counters
constexpr int FirstTaxCounterId = TaxCounter1;
constexpr int LastTaxCounterId = TaxCounter5;
constexpr int TaxCounterCount = LastTaxCounterId - FirstTaxCounterId + 1;
/** @brief Returns true if the given ID is a reserved tax counter. */
inline bool isTaxCounter(int id)
{
return id >= FirstTaxCounterId && id <= LastTaxCounterId;
}
/** @brief Returns the tax counter index (0-based) for display, or -1 if not a tax counter. */
inline int taxCounterIndex(int id)
{
if (!isTaxCounter(id)) {
return -1;
}
return id - FirstTaxCounterId;
}
/** @brief Returns the tax counter ID for the given 0-based index, or -1 if out of range. */
inline int taxCounterIdFromIndex(int index)
{
if (index < 0 || index >= TaxCounterCount) {
return -1;
}
return FirstTaxCounterId + index;
}
} // namespace CounterIds } // namespace CounterIds
/** /**
* @namespace CounterNames * @namespace CastCountIds
* @brief Reserved counter names for server-managed tax counters. * @brief Cast count index range and utilities.
* *
* Used to reject user-created counters that would spoof system counters. * Cast counts use a separate 1-based index space (1-5), not sharing IDs with regular counters.
*/ */
namespace CounterNames namespace CastCountIds
{ {
constexpr const char *TaxCounter1 = "1st_tax_counter"; constexpr int MinIndex = 1; ///< Minimum valid cast count index
constexpr const char *TaxCounter2 = "2nd_tax_counter"; constexpr int MaxIndex = 5; ///< Maximum valid cast count index
constexpr const char *TaxCounter3 = "3rd_tax_counter"; constexpr int Count = MaxIndex; ///< Total number of cast count slots
constexpr const char *TaxCounter4 = "4th_tax_counter";
constexpr const char *TaxCounter5 = "5th_tax_counter";
/** @brief Returns the name for the given tax counter ID, or nullptr if not a tax counter. */ /** @brief Returns true if the index is in the valid cast count range [1, 5]. */
inline const char *forId(int id) inline bool isValidIndex(int index)
{ {
switch (id) { return index >= MinIndex && index <= MaxIndex;
case CounterIds::TaxCounter1:
return TaxCounter1;
case CounterIds::TaxCounter2:
return TaxCounter2;
case CounterIds::TaxCounter3:
return TaxCounter3;
case CounterIds::TaxCounter4:
return TaxCounter4;
case CounterIds::TaxCounter5:
return TaxCounter5;
default:
return nullptr;
}
} }
/** @brief Returns true if the given name is a reserved tax counter name. */ /** @brief Returns the internal name for a cast count at the given index (e.g. "cast_count_1"). */
inline bool isTaxCounter(const QString &name) inline QString nameForIndex(int index)
{ {
return name == TaxCounter1 || name == TaxCounter2 || name == TaxCounter3 || name == TaxCounter4 || return QString("cast_count_%1").arg(index);
name == TaxCounter5;
} }
} // namespace CounterNames } // namespace CastCountIds
#endif // COCKATRICE_COUNTER_IDS_H #endif // COCKATRICE_COUNTER_IDS_H

View file

@ -6,7 +6,7 @@
* *
* Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters * Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters
* something holds. Applies to counters that are constrained to a non-negative display range, * something holds. Applies to counters that are constrained to a non-negative display range,
* such as card counters and commander tax. Unbounded counters (e.g. a player's life total) * such as card counters and cast counts. Unbounded counters (e.g. a player's life total)
* do not use this limit and may go negative, saturating only at the int range. * do not use this limit and may go negative, saturating only at the int range.
* *
* The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit. * The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit.

View file

@ -18,156 +18,35 @@ namespace
{ {
constexpr int UserCounterId = CounterIds::FirstUserId; constexpr int UserCounterId = CounterIds::FirstUserId;
Server_Counter makeCounter(int id, int count, bool active = true) Server_Counter makeCounter(int id, int count)
{ {
Server_Counter c(id, "c", color(), 20, count); return Server_Counter(id, "c", color(), 20, count);
(void)c.setActive(active);
return c;
} }
} // namespace } // namespace
TEST(EvaluateDelCounter, RejectsWhenGameNotStarted) TEST(EvaluateDelCounter, RejectsWhenGameNotStarted)
{ {
Server_Counter counter = makeCounter(UserCounterId, 0); Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ( EXPECT_EQ(Server_Player::evaluateDelCounter(/*gameStarted=*/false, /*playerConceded=*/false, &counter),
Server_Player::evaluateDelCounter(/*gameStarted=*/false, /*playerConceded=*/false, UserCounterId, &counter),
Response::RespGameNotStarted); Response::RespGameNotStarted);
} }
TEST(EvaluateDelCounter, RejectsWhenPlayerConceded) TEST(EvaluateDelCounter, RejectsWhenPlayerConceded)
{ {
Server_Counter counter = makeCounter(UserCounterId, 0); Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateDelCounter(/*gameStarted=*/true, /*playerConceded=*/true, UserCounterId, &counter), EXPECT_EQ(Server_Player::evaluateDelCounter(/*gameStarted=*/true, /*playerConceded=*/true, &counter),
Response::RespContextError); Response::RespContextError);
} }
TEST(EvaluateDelCounter, RejectsTaxCounters)
{
Server_Counter tax1 = makeCounter(CounterIds::TaxCounter1, 0);
EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, CounterIds::TaxCounter1, &tax1),
Response::RespFunctionNotAllowed);
Server_Counter tax2 = makeCounter(CounterIds::TaxCounter2, 0);
EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, CounterIds::TaxCounter2, &tax2),
Response::RespFunctionNotAllowed);
}
TEST(EvaluateDelCounter, RejectsMissingCounter) TEST(EvaluateDelCounter, RejectsMissingCounter)
{ {
EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, UserCounterId, nullptr), Response::RespNameNotFound); EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, nullptr), Response::RespNameNotFound);
} }
TEST(EvaluateDelCounter, AllowsDeletingUserCounter) TEST(EvaluateDelCounter, AllowsDeletingUserCounter)
{ {
Server_Counter counter = makeCounter(UserCounterId, 7); Server_Counter counter = makeCounter(UserCounterId, 7);
EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, UserCounterId, &counter), Response::RespOk); EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, &counter), Response::RespOk);
}
TEST(EvaluateDelCounter, GameNotStartedTakesPrecedenceOverTaxGuard)
{
Server_Counter tax1 = makeCounter(CounterIds::TaxCounter1, 0);
EXPECT_EQ(Server_Player::evaluateDelCounter(false, false, CounterIds::TaxCounter1, &tax1),
Response::RespGameNotStarted);
}
TEST(EvaluateSetCounterActive, RejectsWhenGameNotStarted)
{
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 0);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(/*gameStarted=*/false, /*playerConceded=*/false,
/*commandZoneEnabled=*/true, CounterIds::TaxCounter1, &counter,
/*requestedActive=*/true, nullptr, nullptr),
Response::RespGameNotStarted);
}
TEST(EvaluateSetCounterActive, RejectsWhenPlayerConceded)
{
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 0);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, /*playerConceded=*/true, true, CounterIds::TaxCounter1,
&counter, true, nullptr, nullptr),
Response::RespContextError);
}
TEST(EvaluateSetCounterActive, RejectsNonTaxCounter)
{
Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(
Server_Player::evaluateSetCounterActive(true, false, true, UserCounterId, &counter, true, nullptr, nullptr),
Response::RespFunctionNotAllowed);
}
TEST(EvaluateSetCounterActive, RejectsWhenCommandZoneDisabled)
{
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 0);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, /*commandZoneEnabled=*/false,
CounterIds::TaxCounter1, &counter, true, nullptr, nullptr),
Response::RespContextError);
}
TEST(EvaluateSetCounterActive, RejectsMissingCounter)
{
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter1, nullptr, true,
nullptr, nullptr),
Response::RespNameNotFound);
}
TEST(EvaluateSetCounterActive, RejectsDisablingWhenTaxAccumulated)
{
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 3);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter1, &counter,
/*requestedActive=*/false, nullptr, nullptr),
Response::RespContextError);
}
TEST(EvaluateSetCounterActive, AllowsEnablingWithAccumulatedTax)
{
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 3);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter1, &counter,
/*requestedActive=*/true, nullptr, nullptr),
Response::RespOk);
}
TEST(EvaluateSetCounterActive, AllowsDisablingWhenCounterIsZero)
{
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 0);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter1, &counter,
/*requestedActive=*/false, nullptr, nullptr),
Response::RespOk);
}
TEST(EvaluateSetCounterActive, AllowsEnabling2ndTaxWhen1stIsActive)
{
Server_Counter tax1 = makeCounter(CounterIds::TaxCounter1, 0, true);
Server_Counter tax2 = makeCounter(CounterIds::TaxCounter2, 0, false);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter2, &tax2,
/*requestedActive=*/true, &tax1, nullptr),
Response::RespOk);
}
TEST(EvaluateSetCounterActive, RejectsEnabling2ndTaxWhen1stIsInactive)
{
Server_Counter tax1 = makeCounter(CounterIds::TaxCounter1, 0, false);
Server_Counter tax2 = makeCounter(CounterIds::TaxCounter2, 0, false);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter2, &tax2,
/*requestedActive=*/true, &tax1, nullptr),
Response::RespContextError);
}
TEST(EvaluateSetCounterActive, AllowsDisabling1stTaxWhen2ndIsInactive)
{
Server_Counter tax1 = makeCounter(CounterIds::TaxCounter1, 0, true);
Server_Counter tax2 = makeCounter(CounterIds::TaxCounter2, 0, false);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter1, &tax1,
/*requestedActive=*/false, nullptr, &tax2),
Response::RespOk);
}
TEST(EvaluateSetCounterActive, RejectsDisabling1stTaxWhen2ndIsActive)
{
Server_Counter tax1 = makeCounter(CounterIds::TaxCounter1, 0, true);
Server_Counter tax2 = makeCounter(CounterIds::TaxCounter2, 0, true);
EXPECT_EQ(Server_Player::evaluateSetCounterActive(true, false, true, CounterIds::TaxCounter1, &tax1,
/*requestedActive=*/false, nullptr, &tax2),
Response::RespContextError);
} }
TEST(EvaluateCreateCounter, RejectsWhenGameNotStarted) TEST(EvaluateCreateCounter, RejectsWhenGameNotStarted)
@ -181,20 +60,6 @@ TEST(EvaluateCreateCounter, RejectsWhenPlayerConceded)
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, /*playerConceded=*/true, "test"), Response::RespContextError); EXPECT_EQ(Server_Player::evaluateCreateCounter(true, /*playerConceded=*/true, "test"), Response::RespContextError);
} }
TEST(EvaluateCreateCounter, RejectsTaxCounterNames)
{
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, CounterNames::TaxCounter1),
Response::RespFunctionNotAllowed);
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, CounterNames::TaxCounter2),
Response::RespFunctionNotAllowed);
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, CounterNames::TaxCounter3),
Response::RespFunctionNotAllowed);
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, CounterNames::TaxCounter4),
Response::RespFunctionNotAllowed);
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, CounterNames::TaxCounter5),
Response::RespFunctionNotAllowed);
}
TEST(EvaluateCreateCounter, AllowsUserCounterName) TEST(EvaluateCreateCounter, AllowsUserCounterName)
{ {
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, "poison"), Response::RespOk); EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, "poison"), Response::RespOk);
@ -203,61 +68,151 @@ TEST(EvaluateCreateCounter, AllowsUserCounterName)
TEST(EvaluateModifyCounter, RejectsWhenGameNotStarted) TEST(EvaluateModifyCounter, RejectsWhenGameNotStarted)
{ {
Server_Counter counter = makeCounter(UserCounterId, 0); Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateModifyCounter(/*gameStarted=*/false, /*playerConceded=*/false, true, UserCounterId, EXPECT_EQ(Server_Player::evaluateModifyCounter(/*gameStarted=*/false, /*playerConceded=*/false, &counter),
&counter),
Response::RespGameNotStarted); Response::RespGameNotStarted);
} }
TEST(EvaluateModifyCounter, RejectsWhenPlayerConceded) TEST(EvaluateModifyCounter, RejectsWhenPlayerConceded)
{ {
Server_Counter counter = makeCounter(UserCounterId, 0); Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, /*playerConceded=*/true, true, UserCounterId, &counter), EXPECT_EQ(Server_Player::evaluateModifyCounter(true, /*playerConceded=*/true, &counter),
Response::RespContextError); Response::RespContextError);
} }
TEST(EvaluateModifyCounter, RejectsMissingCounter) TEST(EvaluateModifyCounter, RejectsMissingCounter)
{ {
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, true, UserCounterId, nullptr), EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, nullptr), Response::RespNameNotFound);
Response::RespNameNotFound);
} }
TEST(EvaluateModifyCounter, AllowsUserCounter) TEST(EvaluateModifyCounter, AllowsUserCounter)
{ {
Server_Counter counter = makeCounter(UserCounterId, 5); Server_Counter counter = makeCounter(UserCounterId, 5);
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, true, UserCounterId, &counter), Response::RespOk); EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, &counter), Response::RespOk);
} }
TEST(EvaluateModifyCounter, RejectsTaxCounterWhenCommandZoneDisabled) // Cast count evaluation tests
TEST(EvaluateCreateCastCount, RejectsWhenGameNotStarted)
{ {
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 0); EXPECT_EQ(Server_Player::evaluateCreateCastCount(/*gameStarted=*/false, /*playerConceded=*/false,
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, /*commandZoneEnabled=*/false, CounterIds::TaxCounter1, /*commandZoneEnabled=*/true, 1, /*exists=*/false,
&counter), /*predecessorExists=*/true),
Response::RespGameNotStarted);
}
TEST(EvaluateCreateCastCount, RejectsWhenPlayerConceded)
{
EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, /*playerConceded=*/true, true, 1, false, true),
Response::RespContextError); Response::RespContextError);
} }
TEST(EvaluateModifyCounter, RejectsInactiveTaxCounter) TEST(EvaluateCreateCastCount, RejectsWhenCommandZoneDisabled)
{ {
Server_Counter counter = makeCounter(CounterIds::TaxCounter2, 0, false); EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, /*commandZoneEnabled=*/false, 1, false, true),
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, /*commandZoneEnabled=*/true, CounterIds::TaxCounter2,
&counter),
Response::RespContextError); Response::RespContextError);
} }
TEST(EvaluateModifyCounter, AllowsActiveTaxCounter) TEST(EvaluateCreateCastCount, RejectsInvalidIndex)
{ {
Server_Counter counter = makeCounter(CounterIds::TaxCounter1, 0, true); EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, true, 0, false, false), Response::RespContextError);
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, /*commandZoneEnabled=*/true, CounterIds::TaxCounter1, EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, true, 6, false, false), Response::RespContextError);
&counter), }
TEST(EvaluateCreateCastCount, RejectsExistingIndex)
{
EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, true, 1, /*exists=*/true, true),
Response::RespContextError);
}
TEST(EvaluateCreateCastCount, RejectsGapInSequence)
{
EXPECT_EQ(
Server_Player::evaluateCreateCastCount(true, false, true, 3, /*exists=*/false, /*predecessorExists=*/false),
Response::RespContextError);
}
TEST(EvaluateCreateCastCount, AllowsNextInSequence)
{
EXPECT_EQ(
Server_Player::evaluateCreateCastCount(true, false, true, 2, /*exists=*/false, /*predecessorExists=*/true),
Response::RespOk); Response::RespOk);
} }
TEST(EvaluateModifyCounter, RejectsMissingTaxCounter) TEST(EvaluateDeleteCastCount, RejectsWhenGameNotStarted)
{ {
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, /*commandZoneEnabled=*/true, CounterIds::TaxCounter1, EXPECT_EQ(Server_Player::evaluateDeleteCastCount(/*gameStarted=*/false, /*playerConceded=*/false,
nullptr), /*commandZoneEnabled=*/true, 1, /*exists=*/true, /*value=*/0,
/*successorExists=*/false),
Response::RespGameNotStarted);
}
TEST(EvaluateDeleteCastCount, RejectsWhenPlayerConceded)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(true, /*playerConceded=*/true, true, 1, true, 0, false),
Response::RespContextError);
}
TEST(EvaluateDeleteCastCount, RejectsWhenCommandZoneDisabled)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(true, false, /*commandZoneEnabled=*/false, 1, true, 0, false),
Response::RespContextError);
}
TEST(EvaluateDeleteCastCount, RejectsMissingIndex)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(true, false, true, 1, /*exists=*/false, 0, false),
Response::RespNameNotFound); Response::RespNameNotFound);
} }
TEST(EvaluateDeleteCastCount, RejectsNonZeroValue)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(true, false, true, 1, true, /*value=*/5, false),
Response::RespContextError);
}
TEST(EvaluateDeleteCastCount, RejectsNonHighestIndex)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(true, false, true, 1, true, 0, /*successorExists=*/true),
Response::RespContextError);
}
TEST(EvaluateDeleteCastCount, AllowsHighestWithZeroValue)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(true, false, true, 2, /*exists=*/true, /*value=*/0,
/*successorExists=*/false),
Response::RespOk);
}
TEST(EvaluateModifyCastCount, RejectsWhenGameNotStarted)
{
EXPECT_EQ(Server_Player::evaluateModifyCastCount(/*gameStarted=*/false, /*playerConceded=*/false,
/*commandZoneEnabled=*/true, 1, /*exists=*/true),
Response::RespGameNotStarted);
}
TEST(EvaluateModifyCastCount, RejectsWhenPlayerConceded)
{
EXPECT_EQ(Server_Player::evaluateModifyCastCount(true, /*playerConceded=*/true, true, 1, true),
Response::RespContextError);
}
TEST(EvaluateModifyCastCount, RejectsWhenCommandZoneDisabled)
{
EXPECT_EQ(Server_Player::evaluateModifyCastCount(true, false, /*commandZoneEnabled=*/false, 1, true),
Response::RespContextError);
}
TEST(EvaluateModifyCastCount, RejectsMissingIndex)
{
EXPECT_EQ(Server_Player::evaluateModifyCastCount(true, false, true, 1, /*exists=*/false),
Response::RespNameNotFound);
}
TEST(EvaluateModifyCastCount, AllowsExistingIndex)
{
EXPECT_EQ(Server_Player::evaluateModifyCastCount(true, false, true, 1, /*exists=*/true), Response::RespOk);
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
::testing::InitGoogleTest(&argc, argv); ::testing::InitGoogleTest(&argc, argv);

View file

@ -49,14 +49,6 @@ TEST(NewCounterId, SkipsReservedRangeWhenOnlyReservedCountersExist)
EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId); EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId);
} }
TEST(NewCounterId, SkipsTaxCounterIds)
{
PlayerFixture f;
f.player.addCounter(new Server_Counter(CounterIds::TaxCounter1, "tax1", color(), 0, 0));
f.player.addCounter(new Server_Counter(CounterIds::TaxCounter2, "tax2", color(), 0, 0));
EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId);
}
TEST(NewCounterId, ReturnsNextIdAboveHighestUserCounter) TEST(NewCounterId, ReturnsNextIdAboveHighestUserCounter)
{ {
PlayerFixture f; PlayerFixture f;

View file

@ -47,7 +47,7 @@ struct CommandZoneTestPlayer : Server_Player
/** /**
* @brief Builds a Server_Player on a minimal fake game whose command zone is toggled by the * @brief Builds a Server_Player on a minimal fake game whose command zone is toggled by the
* constructor argument, then runs setupZones() so getCounters() reflects the result. * constructor argument, then runs setupZones() so getCastCounts() reflects the result.
*/ */
struct SetupFixture struct SetupFixture
{ {
@ -86,55 +86,27 @@ struct SetupFixture
}; };
} // namespace } // namespace
TEST(SetupZonesCommandZone, CreatesAllTaxCountersWhenEnabled) TEST(SetupZonesCommandZone, CreatesFirstCastCountWhenEnabled)
{ {
SetupFixture f(true); SetupFixture f(true);
const QMap<int, Server_Counter *> &counters = f.player.getCounters(); const QMap<int, int> &castCounts = f.player.getCastCounts();
EXPECT_TRUE(f.player.getZones().contains(ZoneNames::COMMAND)); EXPECT_TRUE(f.player.getZones().contains(ZoneNames::COMMAND));
for (int i = 0; i < CounterIds::TaxCounterCount; ++i) { EXPECT_EQ(castCounts.size(), 1);
int id = CounterIds::taxCounterIdFromIndex(i); EXPECT_TRUE(castCounts.contains(1));
ASSERT_TRUE(counters.contains(id)); EXPECT_EQ(castCounts.value(1), 0);
} }
const Server_Counter *tax1 = counters.value(CounterIds::TaxCounter1); TEST(SetupZonesCommandZone, NoCastCountsWhenDisabled)
EXPECT_TRUE(tax1->isActive());
EXPECT_EQ(tax1->getCount(), 0);
for (int i = 1; i < CounterIds::TaxCounterCount; ++i) {
int id = CounterIds::taxCounterIdFromIndex(i);
const Server_Counter *counter = counters.value(id);
EXPECT_FALSE(counter->isActive());
EXPECT_EQ(counter->getCount(), 0);
}
}
TEST(SetupZonesCommandZone, TaxCountersUseBounds)
{
SetupFixture f(true);
Server_Counter *tax1 = f.player.getCounters().value(CounterIds::TaxCounter1);
ASSERT_NE(tax1, nullptr);
EXPECT_TRUE(tax1->setCount(MAX_COUNTER_VALUE + 1000));
EXPECT_EQ(tax1->getCount(), MAX_COUNTER_VALUE);
EXPECT_TRUE(tax1->setCount(-1));
EXPECT_EQ(tax1->getCount(), 0);
}
TEST(SetupZonesCommandZone, NoTaxCountersWhenDisabled)
{ {
SetupFixture f(false); SetupFixture f(false);
const QMap<int, Server_Counter *> &counters = f.player.getCounters(); const QMap<int, int> &castCounts = f.player.getCastCounts();
EXPECT_FALSE(f.player.getZones().contains(ZoneNames::COMMAND)); EXPECT_FALSE(f.player.getZones().contains(ZoneNames::COMMAND));
EXPECT_TRUE(castCounts.isEmpty());
for (int i = 0; i < CounterIds::TaxCounterCount; ++i) { EXPECT_TRUE(f.player.getCounters().contains(0));
int id = CounterIds::taxCounterIdFromIndex(i);
EXPECT_FALSE(counters.contains(id));
}
EXPECT_TRUE(counters.contains(0));
} }
int main(int argc, char **argv) int main(int argc, char **argv)

View file

@ -102,13 +102,13 @@ TEST(ServerCounter, IncrementClampsToCustomBounds)
EXPECT_EQ(c.getCount(), 0); EXPECT_EQ(c.getCount(), 0);
} }
TEST(ServerCounter, CustomBoundsForCommanderTax) TEST(ServerCounter, CustomBoundsForCastCount)
{ {
Server_Counter taxCounter(1, "tax", color(), 20, 0, 0, MAX_COUNTER_VALUE); Server_Counter castCount(1, "cast_count", color(), 20, 0, 0, MAX_COUNTER_VALUE);
EXPECT_TRUE(taxCounter.setCount(1000)); EXPECT_TRUE(castCount.setCount(1000));
EXPECT_EQ(taxCounter.getCount(), MAX_COUNTER_VALUE); EXPECT_EQ(castCount.getCount(), MAX_COUNTER_VALUE);
EXPECT_TRUE(taxCounter.setCount(-5)); EXPECT_TRUE(castCount.setCount(-5));
EXPECT_EQ(taxCounter.getCount(), 0); EXPECT_EQ(castCount.getCount(), 0);
} }
TEST(ServerCounter, SetActiveReportsChangeOnce) TEST(ServerCounter, SetActiveReportsChangeOnce)