This commit is contained in:
DawnFire42 2026-08-12 16:15:53 -04:00 • committed by GitHub
commit 5f2094bc49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 2628 additions and 127 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,6 +67,7 @@ set(cockatrice_SOURCES
src/game_graphics/board/card_item.cpp
src/game/board/card_list.cpp
src/game/board/card_state.cpp
src/game_graphics/board/cast_count_widget.cpp
src/game_graphics/board/counter_general.cpp
src/game/board/counter_state.cpp
src/game_graphics/board/translate_counter_name.cpp
@ -87,6 +88,7 @@ set(cockatrice_SOURCES
src/game/phase.cpp
src/game_graphics/phases_toolbar.cpp
src/game_graphics/player/menu/card_menu.cpp
src/game_graphics/player/menu/command_zone_menu.cpp
src/game_graphics/player/menu/custom_zone_menu.cpp
src/game_graphics/player/menu/grave_menu.cpp
src/game_graphics/player/menu/hand_menu.cpp
@ -114,6 +116,8 @@ set(cockatrice_SOURCES
src/game_graphics/player/player_target.cpp
src/game/replay.cpp
src/game/zones/card_zone_logic.cpp
src/game_graphics/zones/command_zone.cpp
src/game/zones/command_zone_logic.cpp
src/game/zones/hand_zone_logic.cpp
src/game/zones/pile_zone_logic.cpp
src/game/zones/stack_zone_logic.cpp

View file

@ -563,6 +563,9 @@ private:
{"Player/aMoveToBottomLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Bottom of Library"),
parseSequenceString("Ctrl+B"),
ShortcutGroup::Move_selected)},
{"Player/aMoveToCommandZone", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Command Zone"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aMoveToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
@ -594,6 +597,18 @@ private:
{"Player/aViewBottomCards", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Bottom Cards of Library"),
parseSequenceString("Ctrl+Shift+W"),
ShortcutGroup::View)},
{"Player/aIncrease1stCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Increase 1st Cast Count"),
parseSequenceString(""),
ShortcutGroup::Player_Counters)},
{"Player/aDecrease1stCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Decrease 1st Cast Count"),
parseSequenceString(""),
ShortcutGroup::Player_Counters)},
{"Player/aIncrease2ndCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Increase 2nd Cast Count"),
parseSequenceString(""),
ShortcutGroup::Player_Counters)},
{"Player/aDecrease2ndCastCount", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Decrease 2nd Cast Count"),
parseSequenceString(""),
ShortcutGroup::Player_Counters)},
{"Player/aCloseMostRecentZoneView", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Close Recent View"),
parseSequenceString("Esc"),
ShortcutGroup::View)},

View file

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

View file

@ -10,7 +10,13 @@ class CounterState : public QObject
{
Q_OBJECT
public:
CounterState(int id, const QString &name, const QColor &color, int radius, int value, QObject *parent = nullptr);
CounterState(int _id,
const QString &_name,
const QColor &_color,
int _radius,
int _value,
bool _active = true,
QObject *parent = nullptr);
static CounterState *fromProto(const ServerInfo_Counter &counter, QObject *parent = nullptr);
@ -34,11 +40,20 @@ public:
{
return value;
}
/** @brief Returns whether this counter is active (visible and modifiable). */
bool isActive() const
{
return active;
}
void setValue(int newValue);
/** @brief Sets the active (visible) state and emits activeChanged if it changed. */
void setActive(bool newActive);
signals:
void valueChanged(int oldValue, int newValue);
/** @brief Emitted when the counter's active state changes. */
void activeChanged(bool newActive);
private:
int id;
@ -46,6 +61,7 @@ private:
QColor color;
int radius;
int value;
bool active; ///< Inactive counters are hidden; server rejects modification attempts
};
#endif // COCKATRICE_COUNTER_STATE_H

View file

@ -7,16 +7,20 @@
#include "../../game_graphics/zones/table_zone.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include "../board/counter_state.h"
#include "../zones/view_zone_logic.h"
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.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_create_cast_count.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_flip_card.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_move_card.pb.h>
#include <libcockatrice/protocol/pb/command_mulligan.pb.h>
@ -27,9 +31,11 @@
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/clamped_arithmetic.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/expression.h>
#include <libcockatrice/utility/zone_names.h>
@ -48,10 +54,10 @@ PlayerActions::PlayerActions(PlayerLogic *_player)
connect(moveTopCardTimer, &QTimer::timeout, [this]() { actMoveTopCardToPlay(); });
}
void PlayerActions::playCard(CardItem *card, bool faceDown)
PendingCommand *PlayerActions::prepareCardMove(CardItem *card, bool faceDown)
{
if (card == nullptr) {
return;
return nullptr;
}
Command_MoveCard cmd;
@ -63,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
ExactCard exactCard = card->getCard();
if (!exactCard) {
return;
return nullptr;
}
const CardInfo &info = exactCard.getInfo();
@ -94,7 +100,14 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
cmd.set_x(gridPoint.x());
cmd.set_y(gridPoint.y());
}
sendGameCommand(cmd);
return prepareGameCommand(cmd);
}
void PlayerActions::playCard(CardItem *card, bool faceDown)
{
if (PendingCommand *pend = prepareCardMove(card, faceDown)) {
sendGameCommand(pend);
}
}
/**
@ -1631,6 +1644,14 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
}
void PlayerActions::playSelectedCards(QList<CardItem *> selectedCards, const bool faceDown)
{
playSelectedCardsImpl(selectedCards, faceDown, nullptr);
}
void PlayerActions::playSelectedCardsImpl(
QList<CardItem *> selectedCards,
bool faceDown,
const std::function<void(PendingCommand *, const QString &)> &postPlayCallback)
{
// CardIds will get shuffled downwards when cards leave the deck.
// We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play
@ -1640,11 +1661,98 @@ void PlayerActions::playSelectedCards(QList<CardItem *> selectedCards, const boo
for (auto &card : selectedCards) {
if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != ZoneNames::TABLE) {
playCard(card, faceDown);
const QString originalZone = card->getZone()->getName();
PendingCommand *pend = prepareCardMove(card, faceDown);
if (pend == nullptr) {
continue;
}
// Connect before send: a local game processes the command synchronously inside
// sendGameCommand, firing the pend's finished signal before this call returns.
if (postPlayCallback) {
postPlayCallback(pend, originalZone);
}
sendGameCommand(pend);
}
}
}
void PlayerActions::actPlayAndIncrease1stCastCount(QList<CardItem *> selectedCards)
{
playAndIncreaseCastCount(selectedCards, 1);
}
void PlayerActions::actPlayAndIncrease2ndCastCount(QList<CardItem *> selectedCards)
{
playAndIncreaseCastCount(selectedCards, 2);
}
void PlayerActions::playAndIncreaseCastCount(QList<CardItem *> selectedCards, int index)
{
playSelectedCardsImpl(selectedCards, false, [this, index](PendingCommand *pend, const QString &originalZone) {
if (originalZone != ZoneNames::COMMAND || pend == nullptr) {
return;
}
// Gate the increment on the server accepting the move
connect(pend, &PendingCommand::finished, this,
[this, index](const Response &response, const CommandContainer &, const QVariant &) {
if (response.response_code() != Response::RespOk) {
return;
}
CounterState *state = player->getCastCount(index);
if (state) {
Command_IncCastCount cmd;
cmd.set_index(index);
cmd.set_delta(1);
sendGameCommand(cmd);
}
});
});
}
void PlayerActions::sendIncCounter(int counterId, int delta)
{
Command_IncCounter cmd;
cmd.set_counter_id(counterId);
cmd.set_delta(delta);
sendGameCommand(cmd);
}
void PlayerActions::actModifyCastCount(int index, int delta)
{
if (!CastCountIds::isValidIndex(index)) {
return;
}
CounterState *state = player->getCastCount(index);
if (!state) {
return;
}
Command_IncCastCount cmd;
cmd.set_index(index);
cmd.set_delta(delta);
sendGameCommand(cmd);
}
void PlayerActions::actToggleCastCount(int index)
{
if (!CastCountIds::isValidIndex(index)) {
return;
}
CounterState *state = player->getCastCount(index);
if (state) {
// Delete only allowed when value is 0
if (state->getValue() != 0) {
return;
}
Command_DeleteCastCount cmd;
cmd.set_index(index);
sendGameCommand(cmd);
} else {
Command_CreateCastCount cmd;
cmd.set_index(index);
sendGameCommand(cmd);
}
}
void PlayerActions::actPlay(QList<CardItem *> selectedCards)
{
playSelectedCards(selectedCards, false);
@ -1925,6 +2033,18 @@ void PlayerActions::cardMenuAction(QList<CardItem *> selectedCards, CardMenuActi
commandList.append(cmd);
break;
}
case cmMoveToCommandZone: {
auto *cmd = new Command_MoveCard;
cmd->set_start_player_id(startPlayerId);
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone(ZoneNames::COMMAND);
cmd->set_x(0);
cmd->set_y(0);
commandList.append(cmd);
break;
}
case cmMoveToTable: {
// Each card needs its own command because table row, pt, and cipt vary per card
for (const auto &card : cardList) {

View file

@ -17,6 +17,7 @@
#include <QMenu>
#include <QObject>
#include <functional>
#include <libcockatrice/card/relation/card_relation_type.h>
#include <libcockatrice/filters/filter_string.h>
@ -126,6 +127,14 @@ public slots:
void actPlay(QList<CardItem *> selectedCards);
void actPlayFacedown(QList<CardItem *> selectedCards);
/** @brief Plays the selected card and increments the 1st cast count. */
void actPlayAndIncrease1stCastCount(QList<CardItem *> selectedCards);
/** @brief Plays the selected card and increments the 2nd cast count. */
void actPlayAndIncrease2ndCastCount(QList<CardItem *> selectedCards);
/** @brief Modifies a cast count by delta. */
void actModifyCastCount(int index, int delta);
/** @brief Toggles a cast count's existence (create if missing, delete if value is 0). */
void actToggleCastCount(int index);
void actHide(QList<CardItem *> selectedCards);
void actMoveTopCardToPlay();
@ -219,6 +228,9 @@ public slots:
void cardMenuAction(QList<CardItem *> selectedCards, CardMenuActionType type);
private:
/** @brief Sends an increment command for the specified counter. */
void sendIncCounter(int counterId, int delta);
PlayerLogic *player;
int defaultNumberTopCards = 1;
@ -245,6 +257,35 @@ private:
void playSelectedCards(QList<CardItem *> selectedCards, bool faceDown = false);
/**
* @brief Builds the move command for playing a card, returning the prepared (unsent) PendingCommand.
* @param card The card to play
* @param faceDown Whether to play the card face-down
* @return The prepared command, or nullptr if the card cannot be played.
*/
PendingCommand *prepareCardMove(CardItem *card, bool faceDown);
/**
* @brief Shared implementation for playing selected cards with an optional per-card callback.
* @param selectedCards Cards to play
* @param faceDown Whether to play cards face-down
* @param postPlayCallback Called for each card once its move is prepared but before it is sent,
* receiving the move's PendingCommand and the card's *original* zone name (the move may
* change the card's zone). Connect response handling here so it is attached before send.
*/
void
playSelectedCardsImpl(QList<CardItem *> selectedCards,
bool faceDown,
const std::function<void(PendingCommand *, const QString &)> &postPlayCallback = nullptr);
/**
* @brief Plays the selected cards and, for each that came from the command zone and whose move
* the server accepts, increments the given cast count by one.
* @param selectedCards Cards to play
* @param index The cast count index (1-5)
*/
void playAndIncreaseCastCount(QList<CardItem *> selectedCards, int index);
void cmdSetTopCard(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_change_zone_properties.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_token.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_cast_count.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_dump_zone.pb.h>
@ -30,6 +32,7 @@
#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_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_shuffle.pb.h>
#include <libcockatrice/utility/color.h>
@ -264,13 +267,16 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
{
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
if (!ctr) {
CounterState *state = player->getCounters().value(event.counter_id(), nullptr);
if (!state) {
return;
}
int oldValue = ctr->getValue();
ctr->setValue(event.value());
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
int oldValue = state->getValue();
state->setValue(event.value());
if (event.value() != oldValue) {
emit logSetCounter(player, state->getId(), state->getName(), event.value(), oldValue);
}
}
void PlayerEventHandler::eventDelCounter(const Event_DelCounter &event)
@ -278,6 +284,21 @@ void PlayerEventHandler::eventDelCounter(const Event_DelCounter &event)
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)
{
PlayerLogic *zoneOwner = player->getGame()->getPlayerManager()->getPlayers().value(event.zone_owner_id(), 0);
@ -630,6 +651,15 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
case GameEvent::DEL_COUNTER:
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
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:
eventDumpZone(event.GetExtension(Event_DumpZone::ext));
break;

View file

@ -48,6 +48,9 @@ class Event_RollDie;
class Event_SetCardAttr;
class Event_SetCardCounter;
class Event_SetCounter;
class Event_CreateCastCount;
class Event_DeleteCastCount;
class Event_SetCastCount;
class Event_Shuffle;
class Event_GameLogNotice;
@ -158,6 +161,15 @@ public:
/// Delete a player-level counter.
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
@ -246,7 +258,7 @@ signals:
void logUnattachCard(PlayerLogic *player, QString cardName);
void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue);
void logSetTapped(PlayerLogic *player, CardItem *card, bool tapped);
void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue);
void logSetCounter(PlayerLogic *player, int counterId, QString counterName, int value, int oldValue);
void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap);
void logSetPT(PlayerLogic *player, CardItem *card, QString newPT);
void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation);

View file

@ -28,6 +28,7 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_zone.pb.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/counter_ids.h>
PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent)
: QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)),
@ -48,6 +49,9 @@ void PlayerLogic::initializeZones()
bool visibleHand = playerInfo->getLocalOrJudge() ||
(game->getPlayerManager()->isSpectator() && game->getGameMetaInfo()->spectatorsOmniscient());
addZone(new HandZoneLogic(this, ZoneNames::HAND, false, false, visibleHand, this));
if (game->getGameMetaInfo()->proto().enable_command_zone()) {
addZone(new CommandZoneLogic(this, ZoneNames::COMMAND, true, false, true, this));
}
}
PlayerLogic::~PlayerLogic()
@ -104,7 +108,9 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
/* StackZone */
ZoneNames::STACK,
/* HandZone */
ZoneNames::HAND};
ZoneNames::HAND,
/* CommandZone */
ZoneNames::COMMAND};
clearCounters();
emit arrowsClearedLocally();
@ -253,15 +259,17 @@ void PlayerLogic::setDeck(const DeckList &_deck)
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
{
return addCounter(counter.id(), QString::fromStdString(counter.name()),
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count());
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(),
counter.active());
}
CounterState *PlayerLogic::addCounter(int id, const QString &name, const QColor &color, int radius, int value)
CounterState *
PlayerLogic::addCounter(int id, const QString &name, const QColor &color, int radius, int value, bool active)
{
if (counters.contains(id)) {
return nullptr;
}
auto *state = new CounterState(id, name, color, radius, value, this);
auto *state = new CounterState(id, name, color, radius, value, active, this);
counters.insert(id, state);
emit counterAdded(state);
return state;
@ -284,6 +292,49 @@ void PlayerLogic::clearCounters()
}
qDeleteAll(counters);
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

View file

@ -11,6 +11,7 @@
#include "../../interface/widgets/menus/tearoff_menu.h"
#include "../board/arrow_data.h"
#include "../interface/deck_loader/loaded_deck.h"
#include "../zones/command_zone_logic.h"
#include "../zones/hand_zone_logic.h"
#include "../zones/pile_zone_logic.h"
#include "../zones/stack_zone_logic.h"
@ -25,6 +26,7 @@
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/protocol/pb/card_attributes.pb.h>
#include <libcockatrice/protocol/pb/game_event.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/utility/zone_names.h>
inline Q_LOGGING_CATEGORY(PlayerLog, "player");
@ -77,6 +79,9 @@ signals:
void counterAdded(CounterState *state);
void counterRemoved(int counterId);
void rearrangeCounters();
void castCountAdded(int index, CounterState *state);
void castCountRemoved(int index);
void castCountValueChanged(int index, int value);
void activeChanged(bool active);
void zoneIdChanged(int zoneId);
void concededChanged(int playerId, bool conceded);
@ -91,6 +96,10 @@ signals:
public slots:
void setActive(bool _active);
void onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed);
void requestRearrangeCounters()
{
emit rearrangeCounters();
}
public:
PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent);
@ -191,8 +200,21 @@ public:
return qobject_cast<HandZoneLogic *>(zones.value(ZoneNames::HAND));
}
/** @brief Returns the command zone logic, or nullptr if not present. */
CommandZoneLogic *getCommandZone() const
{
return qobject_cast<CommandZoneLogic *>(zones.value(ZoneNames::COMMAND));
}
/** @brief Whether this game has command zone support (determined at game creation). */
bool hasCommandZone() const
{
return getCommandZone() != nullptr;
}
CounterState *addCounter(const ServerInfo_Counter &counter);
CounterState *addCounter(int id, const QString &name, const QColor &color, int radius, int value);
CounterState *
addCounter(int id, const QString &name, const QColor &color, int radius, int value, bool active = true);
void delCounter(int counterId);
void clearCounters();
@ -206,6 +228,16 @@ public:
*/
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);
bool getConceded() const
{
@ -240,6 +272,7 @@ private:
int zoneId;
QMap<QString, CardZoneLogic *> zones;
QMap<int, CounterState *> counters;
QMap<int, CounterState *> castCounts; // index (1-5) -> state
bool dialogSemaphore;
QList<CardItem *> cardsToDelete;

View file

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

View file

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

View file

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

View file

@ -29,9 +29,13 @@ AbstractCounter::AbstractCounter(CounterState *state,
{
setAcceptHoverEvents(true);
connect(state, &CounterState::valueChanged, this, [this](int, int newValue) {
value = newValue;
update();
// Route through the (possibly overridden) virtual setValue so subclasses such as
// 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::activeChanged, this, [this](bool newActive) {
setActive(newActive);
player->requestRearrangeCounters();
});
if (player->getPlayerInfo()->getLocalOrJudge()) {
@ -80,6 +84,38 @@ void AbstractCounter::delCounter()
}
}
void AbstractCounter::setValue(int _value)
{
value = _value;
update();
}
static constexpr int OVERLAY_ALPHA = 191;
static constexpr int HOVER_LIGHTEN_PERCENT = 130;
QColor AbstractCounter::getOverlayColor() const
{
QColor result = color;
result.setAlpha(OVERLAY_ALPHA);
if (hovered) {
result = result.lighter(HOVER_LIGHTEN_PERCENT);
}
return result;
}
void AbstractCounter::setActive(bool _active)
{
active = _active;
setVisible(_active);
if (menu) {
menu->setEnabled(_active);
if (!_active) {
menu->hideTearOffMenu();
}
}
update();
}
void AbstractCounter::retranslateUi()
{
if (aSet) {
@ -128,7 +164,7 @@ void AbstractCounter::refreshShortcuts()
void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (!isUnderMouse() || !player->getPlayerInfo()->getLocalOrJudge()) {
if (!active || !isUnderMouse() || !player->getPlayerInfo()->getLocalOrJudge()) {
event->ignore();
return;
}
@ -159,6 +195,10 @@ void AbstractCounter::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
void AbstractCounter::incrementCounter()
{
if (!active) {
return;
}
Command_IncCounter cmd;
cmd.set_counter_id(id);
cmd.set_delta(static_cast<QAction *>(sender())->data().toInt());
@ -167,6 +207,10 @@ void AbstractCounter::incrementCounter()
void AbstractCounter::setCounter()
{
if (!active) {
return;
}
QWidget *parent = nullptr;
if (auto *view = scene() ? scene()->views().value(0) : nullptr) {
parent = view->window();
@ -181,7 +225,8 @@ void AbstractCounter::setCounter()
deleteLater();
return;
}
if (!ok) {
// Re-check active: exec() spins the event loop in case the counter was deactivated.
if (!ok || !active) {
return;
}

View file

@ -1,8 +1,8 @@
/**
* @file abstract_counter.h
* @ingroup GameGraphicsPlayers
* @brief Abstract base for player counters displayed on the game board.
*/
//! \todo Document this file.
#ifndef COUNTER_H
#define COUNTER_H
@ -20,6 +20,7 @@ class QKeyEvent;
class QMenu;
class QString;
//! \todo Document AbstractCounter class members.
class AbstractCounter : public QObject, public QGraphicsItem, public AbstractPlayerComponent
{
Q_OBJECT
@ -35,6 +36,14 @@ protected:
bool hovered = false;
bool useNameForShortcut;
/**
* @brief Returns the counter's color adjusted for overlay rendering.
*
* Applies semi-transparency and lightens the color when hovered.
* Use this for fill-based counters; SVG-based counters (GeneralCounter) bypass this.
*/
[[nodiscard]] QColor getOverlayColor() const;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
@ -61,6 +70,16 @@ public:
~AbstractCounter() override;
void retranslateUi() override;
/**
* @brief Sets the counter value and triggers a visual update.
*
* Virtual to allow subclass display customization (e.g., CastCountWidget tooltip updates).
* Overflow protection is handled server-side, not in client counter classes.
*
* @param _value The new counter value
*/
virtual void setValue(int _value);
void setShortcutsActive() override;
void setShortcutsInactive() override;
void delCounter();
@ -93,6 +112,26 @@ public:
{
return shownInCounterArea;
}
/** @brief Returns whether this counter is shown and can be modified. */
[[nodiscard]] bool isActive() const
{
return active;
}
/**
* @brief Shows or hides the counter and enables or disables its menu.
*
* Sole owner of both visibility and menu state, so container layout code only positions
* counters and may read isActive(). Note that Qt AND-s visibility with the parent item's,
* 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
*/
virtual void setActive(bool _active);
private:
bool active = true; ///< Whether the counter is shown and modifiable
};
class AbstractCounterDialog : public QInputDialog

View file

@ -0,0 +1,52 @@
#include "cast_count_widget.h"
#include "../../game/board/counter_state.h"
#include "translate_counter_name.h"
#include <QFontDatabase>
#include <QPainter>
static constexpr qreal CORNER_RADIUS = 4.0;
static constexpr qreal FONT_SIZE_RATIO = 0.6;
CastCountWidget::CastCountWidget(CounterState *state, PlayerLogic *player, QGraphicsItem *parent)
: AbstractCounter(state, player, false, false, parent), size(state->getRadius())
{
setCacheMode(DeviceCoordinateCache);
setAcceptHoverEvents(true);
setToolTip(tr("%1: %2").arg(TranslateCounterName::getDisplayName(getName())).arg(getValue()));
}
QRectF CastCountWidget::boundingRect() const
{
return QRectF(0, 0, size, size);
}
void CastCountWidget::paint(QPainter *painter,
[[maybe_unused]] const QStyleOptionGraphicsItem *option,
[[maybe_unused]] QWidget *widget)
{
painter->save();
QRectF rect = boundingRect().adjusted(1, 1, -1, -1);
painter->setPen(Qt::NoPen);
painter->setBrush(getOverlayColor());
painter->drawRoundedRect(rect, CORNER_RADIUS, CORNER_RADIUS);
QFont f = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
f.setPixelSize(static_cast<int>(size * FONT_SIZE_RATIO));
f.setWeight(QFont::Bold);
painter->setFont(f);
painter->setPen(Qt::white);
painter->drawText(rect, Qt::AlignCenter, QString::number(value));
painter->restore();
}
void CastCountWidget::setValue(int _value)
{
int clampedValue = qMax(0, _value);
AbstractCounter::setValue(clampedValue);
setToolTip(tr("%1: %2").arg(TranslateCounterName::getDisplayName(getName())).arg(clampedValue));
}

View file

@ -0,0 +1,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

@ -8,4 +8,9 @@ const QMap<QString, QString> TranslateCounterName::translated = {
{"r", QT_TRANSLATE_NOOP("TranslateCounterName", "Red")},
{"g", QT_TRANSLATE_NOOP("TranslateCounterName", "Green")},
{"x", QT_TRANSLATE_NOOP("TranslateCounterName", "Colorless")},
{"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}};
{"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")},
{"cast_count_1", QT_TRANSLATE_NOOP("TranslateCounterName", "1st Cast Count")},
{"cast_count_2", QT_TRANSLATE_NOOP("TranslateCounterName", "2nd Cast Count")},
{"cast_count_3", QT_TRANSLATE_NOOP("TranslateCounterName", "3rd Cast Count")},
{"cast_count_4", QT_TRANSLATE_NOOP("TranslateCounterName", "4th Cast Count")},
{"cast_count_5", QT_TRANSLATE_NOOP("TranslateCounterName", "5th Cast Count")}};

View file

@ -10,6 +10,7 @@
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/protocol/pb/context_mulligan.pb.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/zone_names.h>
#include <utility>
@ -80,6 +81,8 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
fromStr = tr(" from sideboard");
} else if (zoneName == ZoneNames::STACK) {
fromStr = tr(" from the stack");
} else if (zoneName == ZoneNames::COMMAND) {
fromStr = tr(" from the command zone");
} else {
fromStr = tr(" from custom zone '%1'").arg(zoneName);
}
@ -344,6 +347,8 @@ void MessageLogWidget::logMoveCard(PlayerLogic *player,
} else {
finalStr = tr("%1 plays %2%3.");
}
} else if (targetZoneName == ZoneNames::COMMAND) {
finalStr = tr("%1 moves %2%3 to the command zone.");
} else {
fourthArg = targetZoneName;
if (card->getFaceDown()) {
@ -665,8 +670,9 @@ void MessageLogWidget::logSetCardCounter(PlayerLogic *player, QString cardName,
.arg(value));
}
void MessageLogWidget::logSetCounter(PlayerLogic *player, 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") {
soundEngine->playSound("life_change");
}

View file

@ -90,7 +90,7 @@ public slots:
void logSetActivePlayer(PlayerLogic *player);
void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation);
void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue);
void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue);
void logSetCounter(PlayerLogic *player, int counterId, QString counterName, int value, int oldValue);
void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap);
void logSetPT(PlayerLogic *player, CardItem *card, QString newPT);
void logSetSideboardLock(PlayerLogic *player, bool locked);

View file

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

View file

@ -3,6 +3,7 @@
#include "../../../client/settings/card_counter_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../interface/widgets/tabs/tab_game.h"
#include "../../board/abstract_counter.h"
#include "../../board/card_item.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
@ -15,6 +16,7 @@
#include <QPainter>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/zone_names.h>
/**
@ -82,6 +84,10 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
aUnattach = makeAction(this, [actions, sel]() { actions->actUnattach(sel()); });
aSetAnnotation = makeAction(this, [actions, sel]() { actions->actRequestSetAnnotationDialog(sel()); });
aPlay = makeAction(this, [actions, sel]() { actions->actPlay(sel()); });
aPlayAndIncrease1stCastCount =
makeAction(this, [actions, sel]() { actions->actPlayAndIncrease1stCastCount(sel()); });
aPlayAndIncrease2ndCastCount =
makeAction(this, [actions, sel]() { actions->actPlayAndIncrease2ndCastCount(sel()); });
aPlayFacedown = makeAction(this, [actions, sel]() { actions->actPlayFacedown(sel()); });
aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); });
aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); });
@ -158,6 +164,37 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
} else if (card->getZone()->getName() == ZoneNames::EXILE ||
card->getZone()->getName() == ZoneNames::GRAVE) {
createGraveyardOrExileMenu(writeableCard);
} else if (card->getZone()->getName() == ZoneNames::COMMAND) {
if (writeableCard) {
addAction(aPlay);
// Only offer for single selection: a multi-select would over-count casts
const bool singleSelection = gameScene->selectedCards().size() <= 1;
if (singleSelection && player->getCastCountWidget(1)) {
addAction(aPlayAndIncrease1stCastCount);
}
if (singleSelection && player->getCastCountWidget(2)) {
addAction(aPlayAndIncrease2ndCastCount);
}
// No reveal submenu - command zone is public
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player, this));
addSeparator();
addAction(aAttach);
addAction(aDrawArrow);
} else {
addAction(aDrawArrow);
addSeparator();
addAction(aClone);
}
addSeparator();
addAction(aSelectAll);
addRelatedCardView();
addRelatedCardActions();
} else {
createHandOrCustomZoneMenu(writeableCard);
}
@ -203,7 +240,7 @@ void CardMenu::createTableMenu(bool canModifyCard)
}
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addMenu(new MoveMenu(player, this));
addSeparator();
addAction(aAttach);
if (card->getAttachedTo()) {
@ -253,7 +290,7 @@ void CardMenu::createStackMenu(bool canModifyCard)
addAction(aPlayFacedown);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addMenu(new MoveMenu(player, this));
addSeparator();
addAction(aAttach);
addAction(aDrawArrow);
@ -282,7 +319,7 @@ void CardMenu::createGraveyardOrExileMenu(bool canModifyCard)
addAction(aPlayFacedown);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addMenu(new MoveMenu(player, this));
addSeparator();
addAction(aAttach);
addAction(aDrawArrow);
@ -320,7 +357,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addMenu(new MoveMenu(player, this));
// actions that are really wonky when done from deck or sideboard
if (card->getZone()->getName() == ZoneNames::HAND) {
@ -344,7 +381,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
void CardMenu::createZonelessMenu(bool canModifyCard)
{
if (canModifyCard) {
addMenu(new MoveMenu(player));
addMenu(new MoveMenu(player, this));
}
}
@ -488,6 +525,8 @@ void CardMenu::retranslateUi()
aPlay->setText(tr("&Play"));
aHide->setText(tr("&Hide"));
aPlayFacedown->setText(tr("Play &Face Down"));
aPlayAndIncrease1stCastCount->setText(tr("Play and &Increase 1st Cast Count"));
aPlayAndIncrease2ndCastCount->setText(tr("Play and Increase &2nd Cast Count"));
aRevealToAll->setText(tr("&All players"));
//: Turn sideways or back again
aTap->setText(tr("&Tap / Untap"));

View file

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

View file

@ -0,0 +1,168 @@
#include "command_zone_menu.h"
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../board/abstract_counter.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game_scene.h"
#include "../../zones/command_zone.h"
#include "../player_graphics_item.h"
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/counter_limits.h>
CommandZoneMenu::CommandZoneMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
{
inc1ShortcutKey = QStringLiteral("Player/aIncrease1stCastCount");
dec1ShortcutKey = QStringLiteral("Player/aDecrease1stCastCount");
inc2ShortcutKey = QStringLiteral("Player/aIncrease2ndCastCount");
dec2ShortcutKey = QStringLiteral("Player/aDecrease2ndCastCount");
PlayerLogic *logic = player->getLogic();
if (logic && logic->getPlayerInfo()->getLocalOrJudge()) {
for (int i = 0; i < CastCountCount; ++i) {
int index = i + 1; // 1-based index
aIncrease[i] = new QAction(this);
connect(aIncrease[i], &QAction::triggered, this, [this, index]() {
if (auto *l = player->getLogic()) {
l->getPlayerActions()->actModifyCastCount(index, 1);
}
});
addAction(aIncrease[i]);
aDecrease[i] = new QAction(this);
connect(aDecrease[i], &QAction::triggered, this, [this, index]() {
if (auto *l = player->getLogic()) {
l->getPlayerActions()->actModifyCastCount(index, -1);
}
});
addAction(aDecrease[i]);
addSeparator();
aToggle[i] = new QAction(this);
connect(aToggle[i], &QAction::triggered, this, [this, index]() {
if (auto *l = player->getLogic()) {
l->getPlayerActions()->actToggleCastCount(index);
}
});
addAction(aToggle[i]);
addSeparator();
}
aToggleMinimized = new QAction(this);
connect(aToggleMinimized, &QAction::triggered, this, &CommandZoneMenu::actToggleMinimized);
addAction(aToggleMinimized);
connect(this, &QMenu::aboutToShow, this, &CommandZoneMenu::updateCastCountActionStates);
}
retranslateUi();
}
void CommandZoneMenu::retranslateUi()
{
setTitle(tr("Co&mmander"));
static const char *ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
for (int i = 0; i < CastCountCount; ++i) {
if (aIncrease[i]) {
aIncrease[i]->setText(tr("&Increase %1 Cast Count (+1)").arg(ordinals[i]));
}
if (aDecrease[i]) {
aDecrease[i]->setText(tr("&Decrease %1 Cast Count (-1)").arg(ordinals[i]));
}
}
if (aToggleMinimized) {
aToggleMinimized->setText(tr("&Minimize"));
}
}
void CommandZoneMenu::actToggleMinimized()
{
CommandZone *zone = player->getCommandZoneGraphicsItem();
if (zone) {
zone->toggleMinimized();
}
}
void CommandZoneMenu::updateCastCountActionStates()
{
static const char *ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
std::array<AbstractCounter *, CastCountCount> castCounts{};
for (int i = 0; i < CastCountCount; ++i) {
castCounts[i] = player->getCastCountWidget(i + 1);
}
int highestActive = -1;
for (int i = CastCountCount - 1; i >= 0; --i) {
if (castCounts[i]) {
highestActive = i;
break;
}
}
for (int i = 0; i < CastCountCount; ++i) {
AbstractCounter *counter = castCounts[i];
if (aIncrease[i]) {
aIncrease[i]->setVisible(counter && counter->getValue() < MAX_COUNTER_VALUE);
}
if (aDecrease[i]) {
aDecrease[i]->setVisible(counter && counter->getValue() > 0);
}
if (aToggle[i]) {
aToggle[i]->setText(counter ? tr("&Remove %1 Cast Count").arg(ordinals[i])
: tr("&Add %1 Cast Count").arg(ordinals[i]));
bool canAdd = !counter && (i == 0 || castCounts[i - 1]);
bool canRemove = counter && counter->getValue() == 0 && i == highestActive;
aToggle[i]->setVisible(canAdd || canRemove);
}
}
if (aToggleMinimized) {
CommandZone *zone = player->getCommandZoneGraphicsItem();
aToggleMinimized->setText(zone && zone->isMinimized() ? tr("&Restore") : tr("&Minimize"));
}
}
void CommandZoneMenu::setShortcutsActive()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
if (aIncrease[0]) {
aIncrease[0]->setShortcuts(shortcuts.getShortcut(inc1ShortcutKey));
}
if (aDecrease[0]) {
aDecrease[0]->setShortcuts(shortcuts.getShortcut(dec1ShortcutKey));
}
if (aIncrease[1]) {
aIncrease[1]->setShortcuts(shortcuts.getShortcut(inc2ShortcutKey));
}
if (aDecrease[1]) {
aDecrease[1]->setShortcuts(shortcuts.getShortcut(dec2ShortcutKey));
}
}
void CommandZoneMenu::setShortcutsInactive()
{
if (aIncrease[0]) {
aIncrease[0]->setShortcut(QKeySequence());
}
if (aDecrease[0]) {
aDecrease[0]->setShortcut(QKeySequence());
}
if (aIncrease[1]) {
aIncrease[1]->setShortcut(QKeySequence());
}
if (aDecrease[1]) {
aDecrease[1]->setShortcut(QKeySequence());
}
}

View file

@ -0,0 +1,61 @@
/**
* @file command_zone_menu.h
* @ingroup GameGraphicsPlayers
* @brief Submenu for command zone actions (cast counts, minimize/restore).
*/
#ifndef COCKATRICE_COMMAND_ZONE_MENU_H
#define COCKATRICE_COMMAND_ZONE_MENU_H
#include "abstract_player_component.h"
#include <QMenu>
#include <array>
#include <libcockatrice/utility/counter_ids.h>
class PlayerGraphicsItem;
/**
* @class CommandZoneMenu
* @brief Player submenu for managing cast counts and command zone display.
*
* Provides actions to increment/decrement each cast count, toggle cast counts
* on/off, and minimize/restore the command zone display.
*
* @see PlayerMenu
* @see CommandZone
*/
class CommandZoneMenu : public QMenu, public AbstractPlayerComponent
{
Q_OBJECT
public:
explicit CommandZoneMenu(PlayerGraphicsItem *player, QMenu *playerMenu);
void retranslateUi() override;
void setShortcutsActive() override;
void setShortcutsInactive() override;
private:
static constexpr int CastCountCount = CastCountIds::Count;
std::array<QAction *, CastCountCount> aIncrease{};
std::array<QAction *, CastCountCount> aDecrease{};
std::array<QAction *, CastCountCount> aToggle{};
QAction *aToggleMinimized = nullptr;
public slots:
void updateCastCountActionStates();
private slots:
void actToggleMinimized();
private:
PlayerGraphicsItem *player;
QString inc1ShortcutKey;
QString dec1ShortcutKey;
QString inc2ShortcutKey;
QString dec2ShortcutKey;
};
#endif // COCKATRICE_COMMAND_ZONE_MENU_H

View file

@ -6,7 +6,7 @@
#include "../card_menu_action_type.h"
#include "../player_graphics_item.h"
MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
MoveMenu::MoveMenu(PlayerGraphicsItem *player, QWidget *parent) : QMenu(tr("Move to"), parent)
{
aMoveToTopLibrary = new QAction(this);
aMoveToTopLibrary->setData(cmMoveToTopLibrary);
@ -21,6 +21,8 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
aMoveToGraveyard->setData(cmMoveToGraveyard);
aMoveToExile = new QAction(this);
aMoveToExile->setData(cmMoveToExile);
aMoveToCommandZone = new QAction(this);
aMoveToCommandZone->setData(cmMoveToCommandZone);
auto *actions = player->getLogic()->getPlayerActions();
@ -38,6 +40,7 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
connect(aMoveToHand, &QAction::triggered, actions, invoke(cmMoveToHand));
connect(aMoveToGraveyard, &QAction::triggered, actions, invoke(cmMoveToGraveyard));
connect(aMoveToExile, &QAction::triggered, actions, invoke(cmMoveToExile));
connect(aMoveToCommandZone, &QAction::triggered, actions, invoke(cmMoveToCommandZone));
addAction(aMoveToTopLibrary);
addAction(aMoveToXfromTopOfLibrary);
@ -50,6 +53,9 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
addAction(aMoveToGraveyard);
addSeparator();
addAction(aMoveToExile);
addSeparator();
addAction(aMoveToCommandZone);
aMoveToCommandZone->setVisible(player->getLogic()->hasCommandZone());
setShortcutsActive();
@ -66,6 +72,7 @@ void MoveMenu::setShortcutsActive()
aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand"));
aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard"));
aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile"));
aMoveToCommandZone->setShortcuts(shortcuts.getShortcut("Player/aMoveToCommandZone"));
}
void MoveMenu::retranslateUi()
@ -77,4 +84,5 @@ void MoveMenu::retranslateUi()
aMoveToHand->setText(tr("&Hand"));
aMoveToGraveyard->setText(tr("&Graveyard"));
aMoveToExile->setText(tr("&Exile"));
aMoveToCommandZone->setText(tr("&Command Zone"));
}

View file

@ -14,7 +14,7 @@ class MoveMenu : public QMenu
Q_OBJECT
public:
explicit MoveMenu(PlayerGraphicsItem *player);
explicit MoveMenu(PlayerGraphicsItem *player, QWidget *parent);
void setShortcutsActive();
void retranslateUi();
@ -26,6 +26,7 @@ public:
QAction *aMoveToTable = nullptr;
QAction *aMoveToGraveyard = nullptr;
QAction *aMoveToExile = nullptr;
QAction *aMoveToCommandZone = nullptr;
};
#endif // COCKATRICE_MOVE_MENU_H

View file

@ -6,6 +6,7 @@
#include "../../../game_graphics/zones/table_zone.h"
#include "../../../interface/widgets/tabs/tab_game.h"
#include "../../board/card_item.h"
#include "../../zones/command_zone.h"
#include "../player_graphics_item.h"
#include "card_menu.h"
#include "hand_menu.h"
@ -32,6 +33,13 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) {
sideboardMenu = addManagedMenu<SideboardMenu>(player, playerMenu);
if (player->getLogic()->hasCommandZone()) {
commandZoneMenu = addManagedMenu<CommandZoneMenu>(player, playerMenu);
} else {
commandZoneMenu = nullptr;
}
customZonesMenu = addManagedMenu<CustomZoneMenu>(player);
playerMenu->addSeparator();
@ -40,6 +48,7 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
utilityMenu = createManagedComponent<UtilityMenu>(player, playerMenu);
} else {
sideboardMenu = nullptr;
commandZoneMenu = nullptr;
customZonesMenu = nullptr;
countersMenu = nullptr;
utilityMenu = nullptr;
@ -69,6 +78,10 @@ void PlayerMenu::setMenusForGraphicItems()
player->getHandZoneGraphicsItem()->setMenu(handMenu);
player->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard);
player->getSideboardZoneGraphicsItem()->setMenu(sideboardMenu);
if (auto *commandZone = player->getCommandZoneGraphicsItem()) {
commandZone->setMenu(commandZoneMenu);
}
}
}

View file

@ -8,6 +8,8 @@
#define COCKATRICE_PLAYER_MENU_H
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "../../game/player/player_logic.h"
#include "command_zone_menu.h"
#include "custom_zone_menu.h"
#include "grave_menu.h"
#include "hand_menu.h"
@ -69,6 +71,11 @@ public:
return utilityMenu;
}
[[nodiscard]] CommandZoneMenu *getCommandZoneMenu() const
{
return commandZoneMenu;
}
[[nodiscard]] bool getShortcutsActive() const
{
return shortcutsActive;
@ -91,6 +98,7 @@ private:
UtilityMenu *utilityMenu;
TallyMenu *tallyMenu;
SayMenu *sayMenu;
CommandZoneMenu *commandZoneMenu;
CustomZoneMenu *customZonesMenu;
/** @brief Drives AbstractPlayerComponent lifecycle delegation. Counters are iterated separately via

View file

@ -3,8 +3,11 @@
#include "../../game/player/player_actions.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_card_item.h"
#include "../board/cast_count_widget.h"
#include "../board/counter_general.h"
#include "../hand_counter.h"
#include "../z_values.h"
#include "../zones/command_zone.h"
#include "../zones/hand_zone.h"
#include "../zones/pile_zone.h"
#include "../zones/stack_zone.h"
@ -14,6 +17,7 @@
#include <QGraphicsView>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/counter_ids.h>
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
{
@ -28,6 +32,8 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
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);
@ -122,6 +128,15 @@ void PlayerGraphicsItem::initializeZones()
new HandZone(player->getHandZone(), static_cast<int>(tableZoneGraphicsItem->boundingRect().height()), this);
connect(player->getPlayerActions(), &PlayerActions::requestSortHand, handZoneGraphicsItem, &HandZone::sortHand);
// Command zone (only created for commander games)
if (auto *commandZoneLogic = player->getCommandZone()) {
commandZoneGraphicsItem = new CommandZone(commandZoneLogic, ZoneSizes::COMMAND_ZONE_HEIGHT, this);
connect(commandZoneGraphicsItem, &CommandZone::minimizedChanged, this, &PlayerGraphicsItem::rearrangeZones);
connect(commandZoneGraphicsItem, &CommandZone::effectiveHeightChanged, this,
&PlayerGraphicsItem::rearrangeZones);
zoneGraphicsItems.insert(commandZoneLogic->getName(), commandZoneGraphicsItem);
}
connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter,
&HandCounter::updateNumber);
connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu);
@ -183,6 +198,36 @@ void PlayerGraphicsItem::setMirrored(bool _mirrored)
}
}
QAction *PlayerGraphicsItem::counterMenuInsertAnchor(int counterId) const
{
QMenu *countersMenu = playerMenu->getCountersMenu();
if (!countersMenu) {
return nullptr;
}
const QList<QAction *> inMenu = countersMenu->actions();
// QMap iterates in ascending key order, so this finds the lowest id above counterId.
for (auto it = counterWidgets.upperBound(counterId); it != counterWidgets.constEnd(); ++it) {
QMenu *menu = it.value()->getMenu();
if (menu && inMenu.contains(menu->menuAction())) {
return menu->menuAction();
}
}
return nullptr;
}
void PlayerGraphicsItem::setCounterMenuRegistered(AbstractCounter *widget, bool registered)
{
QMenu *countersMenu = playerMenu->getCountersMenu();
if (!widget || !countersMenu || !widget->getMenu()) {
return;
}
if (registered) {
countersMenu->insertMenu(counterMenuInsertAnchor(widget->getId()), widget->getMenu());
} else {
countersMenu->removeAction(widget->getMenu()->menuAction());
}
}
void PlayerGraphicsItem::onCounterAdded(CounterState *state)
{
AbstractCounter *widget;
@ -193,9 +238,7 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
}
counterWidgets.insert(state->getId(), widget);
if (playerMenu->getCountersMenu() && widget->getMenu()) {
playerMenu->getCountersMenu()->addMenu(widget->getMenu());
}
setCounterMenuRegistered(widget, state->isActive());
if (playerMenu->getShortcutsActive()) {
widget->setShortcutsActive();
@ -204,21 +247,60 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
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)
{
auto *widget = counterWidgets.take(counterId);
if (!widget) {
return;
}
if (playerMenu->getCountersMenu() && widget->getMenu()) {
playerMenu->getCountersMenu()->removeAction(widget->getMenu()->menuAction());
setCounterMenuRegistered(widget, false);
widget->delCounter();
rearrangeCounters();
}
void PlayerGraphicsItem::onCastCountRemoved(int index)
{
auto *widget = castCountWidgets.take(index);
if (!widget) {
return;
}
if (commandZoneGraphicsItem) {
commandZoneGraphicsItem->unregisterCastCount(widget);
}
widget->delCounter();
if (auto *menu = playerMenu->getCommandZoneMenu()) {
menu->updateCastCountActionStates();
}
rearrangeCounters();
}
void PlayerGraphicsItem::rearrangeCounters()
{
if (commandZoneGraphicsItem) {
commandZoneGraphicsItem->rearrangeCastCounts();
}
qreal ySize = boundingRect().y() + 80;
constexpr qreal padding = 5;
for (auto *ctr : counterWidgets.values()) {
@ -231,9 +313,24 @@ void PlayerGraphicsItem::rearrangeCounters()
}
}
AbstractCounter *PlayerGraphicsItem::getCastCountWidget(int index) const
{
return castCountWidgets.value(index, nullptr);
}
void PlayerGraphicsItem::rearrangeZones()
{
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
// Calculate stack height, accounting for command zone if visible
bool commandZoneVisible = commandZoneGraphicsItem && commandZoneGraphicsItem->isVisible();
qreal tableHeight = tableZoneGraphicsItem->boundingRect().height();
qreal stackHeight = tableHeight;
if (commandZoneVisible) {
stackHeight = tableHeight - totalCommandZoneHeight();
}
stackZoneGraphicsItem->setHeight(stackHeight);
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
if (mirrored) {
if (player->getHandZone()->contentsKnown()) {
@ -244,12 +341,12 @@ void PlayerGraphicsItem::rearrangeZones()
handVisible = false;
}
stackZoneGraphicsItem->setPos(base);
positionCommandAndStackZones(base);
base += QPointF(stackZoneGraphicsItem->boundingRect().width(), 0);
tableZoneGraphicsItem->setPos(base);
} else {
stackZoneGraphicsItem->setPos(base);
positionCommandAndStackZones(base);
tableZoneGraphicsItem->setPos(base.x() + stackZoneGraphicsItem->boundingRect().width(), 0);
base += QPointF(0, tableZoneGraphicsItem->boundingRect().height());
@ -269,7 +366,7 @@ void PlayerGraphicsItem::rearrangeZones()
handZoneGraphicsItem->setPos(base);
base += QPointF(handZoneGraphicsItem->boundingRect().width(), 0);
stackZoneGraphicsItem->setPos(base);
positionCommandAndStackZones(base);
base += QPointF(stackZoneGraphicsItem->boundingRect().width(), 0);
tableZoneGraphicsItem->setPos(base);
@ -278,7 +375,6 @@ void PlayerGraphicsItem::rearrangeZones()
handZoneGraphicsItem->updateOrientation();
tableZoneGraphicsItem->reorganizeCards();
updateBoundingRect();
rearrangeCounters();
}
void PlayerGraphicsItem::updateBoundingRect()
@ -298,3 +394,20 @@ void PlayerGraphicsItem::updateBoundingRect()
emit sizeChanged();
}
qreal PlayerGraphicsItem::totalCommandZoneHeight() const
{
if (commandZoneGraphicsItem && commandZoneGraphicsItem->isVisible()) {
return commandZoneGraphicsItem->currentHeight();
}
return 0;
}
void PlayerGraphicsItem::positionCommandAndStackZones(const QPointF &base)
{
bool commandZoneVisible = commandZoneGraphicsItem && commandZoneGraphicsItem->isVisible();
if (commandZoneVisible) {
commandZoneGraphicsItem->setPos(base);
}
stackZoneGraphicsItem->setPos(base.x(), base.y() + (commandZoneVisible ? totalCommandZoneHeight() : 0));
}

View file

@ -12,6 +12,7 @@
#include <QGraphicsObject>
class CommandZone;
class HandZone;
class PileZone;
class PlayerDialogs;
@ -112,12 +113,26 @@ public:
{
return handZoneGraphicsItem;
}
/** @brief Returns the command zone graphics item. */
[[nodiscard]] CommandZone *getCommandZoneGraphicsItem() const
{
return commandZoneGraphicsItem;
}
/** @brief Returns the counter widget for the given counter ID, or nullptr if not found. */
[[nodiscard]] AbstractCounter *getCounterWidget(int counterId) const
{
return counterWidgets.value(counterId, nullptr);
}
/** @brief Returns the cast count widget for the given index (1-5), or nullptr if not found. */
[[nodiscard]] AbstractCounter *getCastCountWidget(int index) const;
public slots:
void onPlayerActiveChanged(bool _active);
void onCustomZoneAdded(QString customZoneName);
void onCounterAdded(CounterState *state);
void onCounterRemoved(int counterId);
void onCastCountAdded(int index, CounterState *state);
void onCastCountRemoved(int index);
void rearrangeCounters();
void retranslateUi();
@ -134,6 +149,7 @@ private:
PlayerArea *playerArea;
PlayerTarget *playerTarget;
QMap<int, AbstractCounter *> counterWidgets;
QMap<int, AbstractCounter *> castCountWidgets;
QMap<QString, CardZone *> zoneGraphicsItems;
PileZone *deckZoneGraphicsItem;
PileZone *sideboardGraphicsItem;
@ -142,10 +158,22 @@ private:
TableZone *tableZoneGraphicsItem;
StackZone *stackZoneGraphicsItem;
HandZone *handZoneGraphicsItem;
CommandZone *commandZoneGraphicsItem = nullptr;
QRectF bRect;
bool mirrored;
bool handVisible = false;
/**
* @brief Returns the menu action a counter's submenu is inserted before to keep the
* Player -> Counters menu ordered by counter id, or nullptr to append.
*/
[[nodiscard]] QAction *counterMenuInsertAnchor(int counterId) const;
/** @brief Adds or removes a counter's submenu in the Player -> Counters menu. */
void setCounterMenuRegistered(AbstractCounter *widget, bool registered);
/** @brief Returns the command zone's display height, or 0 if hidden. */
[[nodiscard]] qreal totalCommandZoneHeight() const;
/** @brief Positions the command and stack zones vertically starting from base. */
void positionCommandAndStackZones(const QPointF &base);
private slots:
void updateBoundingRect();
void rearrangeZones();

View file

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

View file

@ -0,0 +1,187 @@
#include "command_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../interface/theme_manager.h"
#include "../board/abstract_counter.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
#include "../board/cast_count_widget.h"
#include "../z_values.h"
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/utility/counter_ids.h>
CommandZone::CommandZone(CommandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent)
: SelectZone(_logic, parent), zoneHeight(_zoneHeight)
{
connect(themeManager, &ThemeManager::themeChanged, this, &CommandZone::updateBg);
updateBg();
setCacheMode(DeviceCoordinateCache);
setupClipContainer(ZValues::CARD_BASE);
}
CommandZone::~CommandZone()
{
for (AbstractCounter *ctr : castCounts) {
disconnect(ctr, &QObject::destroyed, this, nullptr);
}
}
void CommandZone::updateBg()
{
update();
}
QRectF CommandZone::boundingRect() const
{
return {0, 0, ZoneSizes::COMMAND_ZONE_WIDTH, currentHeight()};
}
qreal CommandZone::currentHeight() const
{
return minimized ? qMax(zoneHeight * MINIMIZED_HEIGHT_RATIO, static_cast<double>(minimumHeight)) : zoneHeight;
}
void CommandZone::setMinimumHeight(int height)
{
if (minimumHeight == height) {
return;
}
// The floor only affects height while minimized; skip relayout if the displayed
// height is unchanged (e.g. a cast count toggled while expanded).
const qreal oldEffectiveHeight = currentHeight();
minimumHeight = height;
if (qFuzzyCompare(1.0 + currentHeight(), 1.0 + oldEffectiveHeight)) {
return;
}
prepareGeometryChange();
updateClipRect();
reorganizeCards();
update();
// effectiveHeightChanged repositions neighbouring zones; that path re-enters with the
// same floor and early-returns above, so it converges.
emit effectiveHeightChanged();
}
bool CommandZone::isMinimized() const
{
return minimized;
}
void CommandZone::toggleMinimized()
{
minimized = !minimized;
prepareGeometryChange();
updateClipRect();
reorganizeCards();
update();
emit minimizedChanged(minimized);
}
void CommandZone::paint(QPainter *painter,
[[maybe_unused]] const QStyleOptionGraphicsItem *option,
[[maybe_unused]] QWidget *widget)
{
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Command, getLogic()->getPlayer()->getZoneId());
QPointF scenePos = mapToScene(QPointF(0, 0));
painter->setBrushOrigin(-scenePos);
painter->fillRect(boundingRect(), brush);
}
void CommandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
CardZoneLogic *startZone,
const QPoint &dropPoint)
{
if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) {
return;
}
int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(index);
cmd.set_y(0);
for (const CardDragItem *item : dragItems) {
if (item) {
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(item->getId());
if (item->isForceFaceDown()) {
cardToMove->set_face_down(true);
}
}
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
}
void CommandZone::reorganizeCards()
{
restoreStaleEscapedCards();
updateClipRect();
const auto &cards = getLogic()->getCards();
if (cards.isEmpty()) {
update();
return;
}
auto params = buildStackParams(MIN_CARD_VISIBLE);
params.allowBottomOverflow = true;
layoutCardsVertically(params);
update();
}
void CommandZone::registerCastCount(AbstractCounter *counter)
{
if (!counter || castCounts.contains(counter)) {
return;
}
castCounts.append(counter);
connect(counter, &QObject::destroyed, this, [this, counter]() { castCounts.removeOne(counter); });
}
void CommandZone::unregisterCastCount(AbstractCounter *counter)
{
if (!counter) {
return;
}
disconnect(counter, &QObject::destroyed, this, nullptr);
castCounts.removeOne(counter);
}
void CommandZone::rearrangeCastCounts()
{
qreal y = CastCountSizes::MARGIN;
for (AbstractCounter *ctr : castCounts) {
ctr->setPos(CastCountSizes::MARGIN, y);
ctr->setZValue(ZValues::CAST_COUNTS);
y += ctr->getRadius() + CastCountSizes::MARGIN;
}
setMinimumHeight(static_cast<int>(y));
}
void CommandZone::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
toggleMinimized();
event->accept();
} else {
SelectZone::mouseDoubleClickEvent(event);
}
}

View file

@ -0,0 +1,117 @@
/**
* @file command_zone.h
* @ingroup GameGraphicsZones
* @brief Graphics layer for the command zone, used for Commander format.
*/
#ifndef COCKATRICE_COMMAND_ZONE_H
#define COCKATRICE_COMMAND_ZONE_H
#include "../../game/zones/command_zone_logic.h"
#include "../card_dimensions.h"
#include "select_zone.h"
#include <QLoggingCategory>
class AbstractCounter;
inline Q_LOGGING_CATEGORY(CommandZoneLog, "command_zone");
/**
* @namespace ZoneSizes
* @brief Size constants for the command zone and its sub-elements.
*/
namespace ZoneSizes
{
/** @brief Height of the command zone (accommodates a card plus padding) */
constexpr qreal COMMAND_ZONE_HEIGHT = CardDimensions::HEIGHT + 8;
/** @brief Width of the command zone (matches stack zone) */
constexpr qreal COMMAND_ZONE_WIDTH = CardDimensions::WIDTH_F * 1.5;
} // namespace ZoneSizes
/**
* @class CommandZone
* @brief Graphics layer for the command zone in Commander format games.
*
* Always visible when enabled. Uses the generic vertical stacking layout
* with bottom overflow enabled. Can be minimized via double-click (25% height,
* or the cast count floor if higher).
*
* @see SelectZone::layoutCardsVertically for the stacking algorithm
* @see CommandZoneLogic for card data management
* @see CastCountWidget for the cast count overlay
*/
class CommandZone : public SelectZone
{
Q_OBJECT
private:
static constexpr double MINIMIZED_HEIGHT_RATIO = 0.25;
int zoneHeight; ///< Full height in pixels when expanded
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 cast counts)
QList<AbstractCounter *> castCounts; ///< Registered cast count widgets
public:
/**
* @brief Constructs a CommandZone graphics item.
* @param _logic Logic layer managing card data
* @param _zoneHeight Zone height in pixels
* @param parent Parent graphics item
*/
CommandZone(CommandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
~CommandZone() override;
/**
* @brief Handles card drops, calculating insertion position from drop point.
* @param dragItems Cards being dragged
* @param startZone Source zone
* @param dropPoint Drop position in local coordinates
*/
void
handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;
/** @brief Returns the bounding rectangle, accounting for minimized state. */
[[nodiscard]] QRectF boundingRect() const override;
/** @brief Paints the zone background using the Commander theme brush. */
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
/** @brief Repositions cards using vertical stacking with bottom overflow. */
void reorganizeCards() override;
/** @brief Toggles between full and minimized height. */
void toggleMinimized();
/** @brief Returns whether the zone is currently minimized. */
[[nodiscard]] bool isMinimized() const;
/** @brief Returns the current display height (full or minimized). */
[[nodiscard]] qreal currentHeight() const;
/** @brief Sets the minimum height floor, e.g. to ensure cast counts remain visible. */
void setMinimumHeight(int height);
/** @brief Registers a cast count widget for layout management. */
void registerCastCount(AbstractCounter *counter);
/**
* @brief Unregisters a cast count widget from layout management.
*
* Call on removal: the widget outlives it via deleteLater(), so unregister synchronously
* to keep it out of the next layout pass.
*/
void unregisterCastCount(AbstractCounter *counter);
/** @brief Lays out visible cast counts vertically in the top-left corner of the command zone. */
void rearrangeCastCounts();
signals:
/** @brief Emitted when the zone's minimized state changes. */
void minimizedChanged(bool isMinimized);
/** @brief Emitted when display height changes without a minimized-state change (e.g. cast count toggled while
* minimized). */
void effectiveHeightChanged();
protected:
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
private slots:
void updateBg();
};
#endif // COCKATRICE_COMMAND_ZONE_H

View file

@ -94,7 +94,7 @@ int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
if (effectiveOffset <= 0.0) {
return 0;
}
return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1);
return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount);
}
void SelectZone::restoreStaleEscapedCards()

View file

@ -103,7 +103,7 @@ protected:
/**
* @brief Computes the card index at a given y-coordinate within the zone's vertical layout.
* Returns 0 if the zone has no cards or the offset is zero.
* Returns 0..cardCount (inclusive), where cardCount means append after the last card.
*/
int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const;

View file

@ -32,6 +32,10 @@ QRectF StackZone::boundingRect() const
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
QPointF scenePos = mapToScene(QPointF(0, 0));
painter->setBrushOrigin(-scenePos);
painter->fillRect(boundingRect(), brush);
}
@ -43,19 +47,7 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
return;
}
const auto &cards = getLogic()->getCards();
int index;
if (startZone == getLogic()) {
// Reordering within the zone: use drop position
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
// Same-zone no-op: don't move a card onto itself
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
return;
}
} else {
// Coming from another zone: append at end (top of stack, rendered on top)
index = static_cast<int>(cards.size());
}
int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());

View file

@ -26,10 +26,12 @@
#define PLAYERZONE_BG_NAME "playerzone"
#define STACKZONE_BG_NAME "stackzone"
#define TABLEZONE_BG_NAME "tablezone"
#define COMMANDZONE_BG_NAME "commandzone"
static const QColor HANDZONE_BG_DEFAULT = QColor(80, 100, 50);
static const QColor TABLEZONE_BG_DEFAULT = QColor(70, 50, 100);
static const QColor PLAYERZONE_BG_DEFAULT = QColor(200, 200, 200);
static const QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43);
static const QColor COMMANDZONE_BG_DEFAULT = QColor(50, 60, 80);
static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"};
struct PaletteColorInfo
@ -424,6 +426,8 @@ void ThemeManager::themeChangedSlot()
brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
brushes[Role::Command] = loadBrush(COMMANDZONE_BG_NAME, COMMANDZONE_BG_DEFAULT);
for (auto &brushCache : brushesCache) {
brushCache.clear();
}
@ -448,6 +452,9 @@ static QString roleBgName(ThemeManager::Role role)
case ThemeManager::Table:
return TABLEZONE_BG_NAME;
case ThemeManager::Command:
return COMMANDZONE_BG_NAME;
default:
Q_ASSERT(false);
return {};

View file

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

View file

@ -103,6 +103,7 @@ void DlgCreateGame::sharedCtor()
startingLifeTotalLabel->setBuddy(startingLifeTotalEdit);
shareDecklistsOnLoadCheckBox = new QCheckBox(tr("Open decklists in lobby"));
enableCommandZoneCheckBox = new QCheckBox(tr("Enable command zone"));
createGameAsJudgeCheckBox = new QCheckBox(tr("Create game as judge"));
@ -110,8 +111,9 @@ void DlgCreateGame::sharedCtor()
gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0);
gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1);
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0);
gameSetupOptionsLayout->addWidget(enableCommandZoneCheckBox, 2, 0);
if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0);
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 3, 0);
} else {
createGameAsJudgeCheckBox->setChecked(false);
createGameAsJudgeCheckBox->setHidden(true);
@ -172,6 +174,7 @@ DlgCreateGame::DlgCreateGame(TabRoom *_room, const QMap<int, QString> &_gameType
createGameAsSpectatorCheckBox->setChecked(SettingsCache::instance().game().getCreateGameAsSpectator());
startingLifeTotalEdit->setValue(SettingsCache::instance().game().getDefaultStartingLifeTotal());
shareDecklistsOnLoadCheckBox->setChecked(SettingsCache::instance().game().getShareDecklistsOnLoad());
enableCommandZoneCheckBox->setChecked(SettingsCache::instance().game().getEnableCommandZone());
if (!rememberGameSettings->isChecked()) {
actReset();
@ -205,6 +208,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
createGameAsSpectatorCheckBox->setEnabled(false);
startingLifeTotalEdit->setEnabled(false);
shareDecklistsOnLoadCheckBox->setEnabled(false);
enableCommandZoneCheckBox->setEnabled(false);
descriptionEdit->setText(QString::fromStdString(gameInfo.description()));
maxPlayersEdit->setValue(gameInfo.max_players());
@ -215,6 +219,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat());
spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient());
shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load());
enableCommandZoneCheckBox->setChecked(gameInfo.enable_command_zone());
QSet<int> types;
for (int i = 0; i < gameInfo.game_types_size(); ++i) {
@ -252,6 +257,7 @@ void DlgCreateGame::actReset()
startingLifeTotalEdit->setValue(20);
shareDecklistsOnLoadCheckBox->setChecked(false);
enableCommandZoneCheckBox->setChecked(false);
createGameAsJudgeCheckBox->setChecked(false);
QMapIterator<int, QRadioButton *> gameTypeCheckBoxIterator(gameTypeCheckBoxes);
@ -282,6 +288,7 @@ void DlgCreateGame::actOK()
cmd.set_join_as_spectator(createGameAsSpectatorCheckBox->isChecked());
cmd.set_starting_life_total(startingLifeTotalEdit->value());
cmd.set_share_decklists_on_load(shareDecklistsOnLoadCheckBox->isChecked());
cmd.set_enable_command_zone(enableCommandZoneCheckBox->isChecked());
auto _gameTypes = QString();
QMapIterator<int, QRadioButton *> gameTypeCheckBoxIterator(gameTypeCheckBoxes);
@ -306,6 +313,7 @@ void DlgCreateGame::actOK()
SettingsCache::instance().game().setCreateGameAsSpectator(createGameAsSpectatorCheckBox->isChecked());
SettingsCache::instance().game().setDefaultStartingLifeTotal(startingLifeTotalEdit->value());
SettingsCache::instance().game().setShareDecklistsOnLoad(shareDecklistsOnLoadCheckBox->isChecked());
SettingsCache::instance().game().setEnableCommandZone(enableCommandZoneCheckBox->isChecked());
SettingsCache::instance().game().setGameTypes(_gameTypes);
}
PendingCommand *pend = room->prepareRoomCommand(cmd);

View file

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

View file

@ -45,6 +45,9 @@ DlgFilterGames::DlgFilterGames(const QMap<int, QString> &_allGameTypes,
hideOpenDecklistGames = new QCheckBox(tr("Hide games with forced open decklists"));
hideOpenDecklistGames->setChecked(filters.hideOpenDecklistGames);
hideCommandZoneGames = new QCheckBox(tr("Hide games with command zone enabled"));
hideCommandZoneGames->setChecked(filters.hideCommandZoneGames);
maxGameAgeComboBox = new QComboBox();
maxGameAgeComboBox->setEditable(false);
maxGameAgeComboBox->addItems(gameAgeMap.values());
@ -122,6 +125,7 @@ DlgFilterGames::DlgFilterGames(const QMap<int, QString> &_allGameTypes,
restrictionsLayout->addWidget(hideIgnoredUserGames, 4, 0);
restrictionsLayout->addWidget(hideNotBuddyCreatedGames, 5, 0);
restrictionsLayout->addWidget(hideOpenDecklistGames, 6, 0);
restrictionsLayout->addWidget(hideCommandZoneGames, 7, 0);
auto *restrictionsGroupBox = new QGroupBox(tr("Restrictions"));
restrictionsGroupBox->setLayout(restrictionsLayout);
@ -192,6 +196,7 @@ GameFilterConfigs DlgFilterGames::getFilters() const
hidePasswordProtectedGames->isChecked(),
hideNotBuddyCreatedGames->isChecked(),
hideOpenDecklistGames->isChecked(),
hideCommandZoneGames->isChecked(),
gameNameFilterEdit->text(),
getCreatorNameFilters(),
getGameTypeFilter(),

View file

@ -34,6 +34,7 @@ private:
QCheckBox *hideIgnoredUserGames;
QCheckBox *hideNotBuddyCreatedGames;
QCheckBox *hideOpenDecklistGames;
QCheckBox *hideCommandZoneGames;
QLineEdit *gameNameFilterEdit;
QLineEdit *creatorNameFilterEdit;
QMap<int, QCheckBox *> gameTypeFilterCheckBoxes;

View file

@ -34,10 +34,13 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
startingLifeTotalEdit->setValue(20);
startingLifeTotalLabel->setBuddy(startingLifeTotalEdit);
enableCommandZoneCheckBox = new QCheckBox(tr("Enable command zone"), this);
auto *gameSetupGrid = new QGridLayout;
gameSetupGrid->setContentsMargins(5, 5, 5, 5);
gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0);
gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1);
gameSetupGrid->addWidget(enableCommandZoneCheckBox, 1, 0, 1, 2);
gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this);
gameSetupOptionsGroupBox->setLayout(gameSetupGrid);
@ -58,6 +61,7 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
if (rememberSettingsCheckBox->isChecked()) {
numberPlayersEdit->setValue(SettingsCache::instance().game().getLocalGameMaxPlayers());
startingLifeTotalEdit->setValue(SettingsCache::instance().game().getLocalGameStartingLifeTotal());
enableCommandZoneCheckBox->setChecked(SettingsCache::instance().game().getLocalGameEnableCommandZone());
}
setWindowTitle(tr("Local game options"));
@ -72,6 +76,7 @@ void DlgLocalGameOptions::actOK()
if (rememberSettingsCheckBox->isChecked()) {
SettingsCache::instance().game().setLocalGameMaxPlayers(numberPlayersEdit->value());
SettingsCache::instance().game().setLocalGameStartingLifeTotal(startingLifeTotalEdit->value());
SettingsCache::instance().game().setLocalGameEnableCommandZone(enableCommandZoneCheckBox->isChecked());
}
accept();
@ -82,5 +87,6 @@ LocalGameOptions DlgLocalGameOptions::getOptions() const
return LocalGameOptions{
.numberPlayers = numberPlayersEdit->value(),
.startingLifeTotal = startingLifeTotalEdit->value(),
.enableCommandZone = enableCommandZoneCheckBox->isChecked(),
};
}

View file

@ -16,6 +16,7 @@ struct LocalGameOptions
{
int numberPlayers = 1;
int startingLifeTotal = 20;
bool enableCommandZone = false;
};
class QCheckBox;
@ -45,6 +46,7 @@ private:
QLabel *startingLifeTotalLabel;
QSpinBox *startingLifeTotalEdit;
QCheckBox *enableCommandZoneCheckBox;
QCheckBox *rememberSettingsCheckBox;
QDialogButtonBox *buttonBox;

View file

@ -18,6 +18,7 @@ struct GameFilterConfigs
bool hidePasswordProtectedGames = false;
bool hideNotBuddyCreatedGames = false;
bool hideOpenDecklistGames = false;
bool hideCommandZoneGames = false;
QString gameNameFilter = "";
QStringList creatorNameFilters = {};
QSet<int> gameTypeFilter = {};

View file

@ -158,6 +158,9 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const
if (gameentry.share_decklists_on_load()) {
result.append(tr("open decklists"));
}
if (gameentry.enable_command_zone()) {
result.append(tr("command zone"));
}
return result.join(", ");
}
case Qt::DecorationRole: {
@ -346,9 +349,9 @@ void GamesProxyModel::loadFilterParameters(const QMap<int, QString> &allGameType
setGameFilters({gameFilters.isHideBuddiesOnlyGames(), gameFilters.isHideIgnoredUserGames(),
gameFilters.isHideFullGames(), gameFilters.isHideGamesThatStarted(),
gameFilters.isHidePasswordProtectedGames(), gameFilters.isHideNotBuddyCreatedGames(),
gameFilters.isHideOpenDecklistGames(), gameFilters.getGameNameFilter(),
gameFilters.getCreatorNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(),
gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(),
gameFilters.isHideOpenDecklistGames(), gameFilters.isHideCommandZoneGames(),
gameFilters.getGameNameFilter(), gameFilters.getCreatorNameFilters(), newGameTypeFilter,
gameFilters.getMinPlayers(), gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(),
gameFilters.isShowOnlyIfSpectatorsCanWatch(), gameFilters.isShowSpectatorPasswordProtected(),
gameFilters.isShowOnlyIfSpectatorsCanChat(), gameFilters.isShowOnlyIfSpectatorsCanSeeHands()});
}
@ -363,6 +366,7 @@ void GamesProxyModel::saveFilterParameters(const QMap<int, QString> &allGameType
gameFilters.setHideIgnoredUserGames(filters.hideIgnoredUserGames);
gameFilters.setHideNotBuddyCreatedGames(filters.hideNotBuddyCreatedGames);
gameFilters.setHideOpenDecklistGames(filters.hideOpenDecklistGames);
gameFilters.setHideCommandZoneGames(filters.hideCommandZoneGames);
gameFilters.setGameNameFilter(filters.gameNameFilter);
gameFilters.setCreatorNameFilters(filters.creatorNameFilters);
@ -408,6 +412,9 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
if (filters.hideOpenDecklistGames && game.share_decklists_on_load()) {
return false;
}
if (filters.hideCommandZoneGames && game.enable_command_zone()) {
return false;
}
if (filters.hideIgnoredUserGames &&
userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) {
return false;

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -268,7 +268,10 @@ Client
**Server:**
- `Server_Player::cmdIncCounter`
- Rejects if the game has not started or the player has conceded
- Updates the counter value
- Rejects tax counters when command zone is disabled (`RespContextError`)
- Rejects inactive tax counters (`RespContextError`)
- Rejects if counter doesn't exist (`RespNameNotFound`)
- Updates the counter value (clamped to `[minValue, maxValue]`)
- Emits `Event_SetCounter` only if the value changed
**Client:**
@ -285,7 +288,8 @@ Client
**Server:**
- `Server_Player::cmdCreateCounter`
- Rejects if the game has not started or the player has conceded
- Allocates a new counter ID
- Rejects reserved tax counter names (`RespFunctionNotAllowed`)
- Allocates a new counter ID (starting at `CounterIds::FirstUserId`)
- Creates the counter
- Emits `Event_CreateCounter`
@ -302,7 +306,10 @@ Client
**Server:**
- `Server_Player::cmdSetCounter`
- Rejects if the game has not started or the player has conceded
- Updates the counter value
- Rejects tax counters when command zone is disabled (`RespContextError`)
- Rejects inactive tax counters (`RespContextError`)
- Rejects if counter doesn't exist (`RespNameNotFound`)
- Updates the counter value (clamped to `[minValue, maxValue]`)
- Emits `Event_SetCounter` only if the value changed
**Client:**
@ -319,6 +326,8 @@ Client
**Server:**
- `Server_Player::cmdDelCounter`
- Rejects if the game has not started or the player has conceded
- Rejects tax counters (`RespFunctionNotAllowed`)
- Rejects if counter doesn't exist (`RespNameNotFound`)
- Deletes the counter
- Emits `Event_DelCounter`
@ -521,6 +530,26 @@ Client
---
### `SET_COUNTER_ACTIVE` (1035)
**Purpose:** Show or hide a reserved tax counter without deleting it.
**Server:**
- `Server_Player::cmdSetCounterActive`
- Rejects if game not started (`RespGameNotStarted`)
- Rejects if player has conceded (`RespContextError`)
- Rejects for non-tax counters (`RespFunctionNotAllowed`)
- Rejects if command zone is disabled (`RespContextError`)
- Rejects if counter doesn't exist (`RespNameNotFound`)
- Rejects deactivation when counter has non-zero value (`RespContextError`)
- Emits `Event_SetCounterActive` only if the active state changed
**Client:**
- `PlayerEventHandler::eventSetCounterActive`
- Updates the counter's active state in the UI
---
## Notes
- Game commands are handled by `Server_Player`, `Server_AbstractParticipant`, or `Server_Game`, depending on the command.

View file

@ -476,8 +476,7 @@
* @ingroup GameMenus
* @brief Menus for interacting with zones.
*
* Provides contextual options for a CardZone such as the hand,
* library, graveyard, and battlefield.
* Provides contextual options for card zones.
*/
/**
@ -512,8 +511,7 @@
* @ingroup GameGraphics
* @brief Graphical representations of zones.
*
* Provides layout, visuals, and animations for a CardZone like the hand,
* library, battlefield, and graveyard.
* Provides layout, visuals, and animations for card zones.
*/
/**
@ -548,8 +546,7 @@
* @ingroup GameLogic
* @brief Logical handling of CardZones during a Game.
*
* Defines the rules and behaviors of zones such as the hand,
* battlefield, library, and graveyard.
* Defines the rules and behaviors of card zones.
*/
/**

View file

@ -21,9 +21,11 @@ public:
[[nodiscard]] virtual int getDefaultStartingLifeTotal() const = 0;
[[nodiscard]] virtual bool getShareDecklistsOnLoad() const = 0;
[[nodiscard]] virtual bool getRememberGameSettings() const = 0;
[[nodiscard]] virtual bool getEnableCommandZone() const = 0;
[[nodiscard]] virtual bool getLocalGameRememberSettings() const = 0;
[[nodiscard]] virtual int getLocalGameMaxPlayers() const = 0;
[[nodiscard]] virtual int getLocalGameStartingLifeTotal() const = 0;
[[nodiscard]] virtual bool getLocalGameEnableCommandZone() const = 0;
};
#endif // COCKATRICE_INTERFACE_GAME_SETTINGS_PROVIDER_H

View file

@ -13,16 +13,19 @@
#include <libcockatrice/protocol/pb/command_change_zone_properties.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_cast_count.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_deck_select.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_cast_count.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_flip_card.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_cast_count.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_leave_game.pb.h>
@ -36,6 +39,7 @@
#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_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_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
@ -342,6 +346,34 @@ Response::ResponseCode Server_AbstractParticipant::cmdDelCounter(const Command_D
return Response::RespFunctionNotAllowed;
}
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*/,
GameEventStorage & /*ges*/)
{
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_NextTurn & /*cmd*/,
ResponseContainer & /*rc*/,
GameEventStorage & /*ges*/)
@ -525,6 +557,18 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
case GameCommand::REVERSE_TURN:
return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges);
break;
case GameCommand::CREATE_CAST_COUNT:
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;
default:
return Response::RespInvalidCommand;
}

View file

@ -52,6 +52,10 @@ class Command_SetSideboardPlan;
class Command_DeckSelect;
class Command_SetSideboardLock;
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
{
@ -172,6 +176,14 @@ public:
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges);
virtual Response::ResponseCode
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);
void sendGameEvent(const GameEventContainer &event);

View file

@ -2,8 +2,15 @@
#include <libcockatrice/protocol/pb/serverinfo_counter.pb.h>
Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count)
: id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count)
Server_Counter::Server_Counter(int _id,
const QString &_name,
const color &_counterColor,
int _radius,
int _count,
int _minValue,
int _maxValue)
: id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count), minValue(_minValue),
maxValue(_maxValue)
{
}
@ -14,4 +21,5 @@ void Server_Counter::getInfo(ServerInfo_Counter *info)
info->mutable_counter_color()->CopyFrom(counterColor);
info->set_radius(radius);
info->set_count(count);
info->set_active(active);
}

View file

@ -29,13 +29,12 @@ class ServerInfo_Counter;
/**
* @class Server_Counter
* @brief Represents a player counter with overflow-safe increment arithmetic.
* @brief Represents a player counter with overflow-safe increment arithmetic and optional bounds.
*
* All value modifications return whether the value actually changed,
* enabling callers to skip unnecessary network events.
*
* @note Direct assignment via setCount() does not clamp; only
* incrementCount() enforces int boundary saturation.
* @note Values are clamped to [minValue, maxValue] on both setCount() and incrementCount().
* @note Unlike card counters, player counters are never auto-removed
* when they reach zero - they persist with value 0.
*/
@ -47,9 +46,30 @@ protected:
color counterColor;
int radius;
int count;
int minValue; ///< Minimum allowed value (default: INT_MIN, i.e. unbounded)
int maxValue; ///< Maximum allowed value (default: INT_MAX, i.e. unbounded)
bool active = true; ///< Whether this counter is visible/active (default: true)
static constexpr int DEFAULT_MAX_VALUE = std::numeric_limits<int>::max();
public:
Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count = 0);
/**
* @brief Constructs a counter.
* @param _id Unique counter identifier
* @param _name Display name
* @param _counterColor Counter color
* @param _radius Display radius
* @param _count Initial value (default 0)
* @param _minValue Minimum allowed value (default INT_MIN)
* @param _maxValue Maximum allowed value (default INT_MAX)
*/
Server_Counter(int _id,
const QString &_name,
const color &_counterColor,
int _radius,
int _count = 0,
int _minValue = std::numeric_limits<int>::min(),
int _maxValue = DEFAULT_MAX_VALUE);
~Server_Counter()
{
}
@ -73,18 +93,31 @@ public:
{
return count;
}
bool isActive() const
{
return active;
}
/**
* @brief Sets the counter to an exact value.
* @param _count The new value (assigned directly without clamping).
* @return true if the value changed, false otherwise.
* @warning This performs raw assignment. For overflow-safe incrementing,
* use incrementCount().
* @brief Sets the active (visible) state of this counter.
* @param _active True to show the counter, false to hide it
* @return true if the state changed
*/
[[nodiscard]] bool setActive(bool _active)
{
bool oldActive = active;
active = _active;
return active != oldActive;
}
/**
* @brief Sets the counter value, clamping to [minValue, maxValue].
* @param _count The desired new value
* @return true if the clamped value differs from the previous value
* @note For increment operations, prefer incrementCount() which handles overflow safely.
*/
[[nodiscard]] bool setCount(int _count)
{
const int oldCount = count;
count = _count;
int oldCount = count;
count = qBound(minValue, _count, maxValue);
return count != oldCount;
}
@ -92,12 +125,12 @@ public:
* @brief Increments the counter by delta with overflow-safe arithmetic.
* @param delta The amount to add (may be negative for decrement).
* @return true if the value changed, false otherwise.
* @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow.
* @note Clamps result to [minValue, maxValue] to prevent overflow.
*/
[[nodiscard]] bool incrementCount(int delta)
{
const int oldCount = count;
count = addClamped(count, delta, std::numeric_limits<int>::min(), std::numeric_limits<int>::max());
count = addClamped(count, delta, minValue, maxValue);
return count != oldCount;
}

View file

@ -67,6 +67,7 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo,
bool _spectatorsSeeEverything,
int _startingLifeTotal,
bool _shareDecklistsOnLoad,
bool _enableCommandZone,
Server_Room *_room)
: QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)),
gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers),
@ -74,9 +75,9 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo,
onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed),
spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk),
spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal),
shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0),
firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr),
gameMutex()
shareDecklistsOnLoad(_shareDecklistsOnLoad), enableCommandZone(_enableCommandZone), inactivityCounter(0),
startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false),
startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex()
{
currentReplay = new GameReplay;
currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId());
@ -854,6 +855,7 @@ void Server_Game::getInfo(ServerInfo_Game &result) const
result.set_spectators_can_chat(spectatorsCanTalk);
result.set_spectators_omniscient(spectatorsSeeEverything);
result.set_share_decklists_on_load(shareDecklistsOnLoad);
result.set_enable_command_zone(enableCommandZone);
result.set_spectators_count(getSpectatorCount());
result.set_start_time(startTime.toSecsSinceEpoch());
}

View file

@ -69,6 +69,7 @@ private:
bool spectatorsSeeEverything;
int startingLifeTotal;
bool shareDecklistsOnLoad;
bool enableCommandZone;
int inactivityCounter;
int startTimeOfThisGame, secondsElapsed;
bool firstGameStarted;
@ -106,6 +107,7 @@ public:
bool _spectatorsSeeEverything,
int _startingLifeTotal,
bool _shareDecklistsOnLoad,
bool _enableCommandZone,
Server_Room *parent);
~Server_Game() override;
Server_Room *getRoom() const
@ -173,6 +175,10 @@ public:
{
return shareDecklistsOnLoad;
}
bool getEnableCommandZone() const
{
return enableCommandZone;
}
Response::ResponseCode
checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge);
bool containsUser(const QString &userName) const;

View file

@ -18,14 +18,18 @@
#include <libcockatrice/protocol/pb/command_attach_card.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_create_cast_count.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_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_inc_cast_count.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_mulligan.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_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
@ -33,20 +37,26 @@
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
#include <libcockatrice/protocol/pb/context_mulligan.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_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_game_log_notice.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_shuffle.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.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_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/string_limits.h>
#include <libcockatrice/utility/zone_names.h>
@ -71,7 +81,7 @@ int Server_Player::newCounterId() const
id = c->getId();
}
}
return id + 1;
return std::max(id + 1, CounterIds::FirstUserId);
}
void Server_Player::setupZones()
@ -101,6 +111,12 @@ void Server_Player::setupZones()
addCounter(new Server_Counter(6, "x", makeColor(255, 255, 255), 20, 0));
addCounter(new Server_Counter(7, "storm", makeColor(255, 150, 30), 20, 0));
// Command zone for Commander format
if (game->getEnableCommandZone()) {
addZone(new Server_CardZone(this, ZoneNames::COMMAND, false, ServerInfo_Zone::PublicZone));
castCounts.insert(1, 0);
}
// ------------------------------------------------------------------
// Assign card ids and create deck from deck list
@ -155,6 +171,7 @@ void Server_Player::clearZones()
delete counter;
}
counters.clear();
castCounts.clear();
lastDrawList.clear();
}
@ -427,19 +444,29 @@ Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer &
}
Response::ResponseCode
Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
Server_Player::evaluateModifyCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter)
{
if (!game->getGameStarted()) {
if (!gameStarted) {
return Response::RespGameNotStarted;
}
if (conceded) {
if (playerConceded) {
return Response::RespContextError;
}
if (!counter) {
return Response::RespNameNotFound;
}
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
const int counterId = cmd.counter_id();
Server_Counter *c = counters.value(counterId, nullptr);
if (!c) {
return Response::RespNameNotFound;
const Response::ResponseCode authResult = evaluateModifyCounter(game->getGameStarted(), conceded, c);
if (authResult != Response::RespOk) {
return authResult;
}
bool didChange = c->incrementCount(cmd.delta());
@ -454,17 +481,28 @@ Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &
}
Response::ResponseCode
Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
Server_Player::evaluateCreateCounter(bool gameStarted, bool playerConceded, const QString & /*counterName*/)
{
if (!game->getGameStarted()) {
if (!gameStarted) {
return Response::RespGameNotStarted;
}
if (conceded) {
if (playerConceded) {
return Response::RespContextError;
}
return Response::RespOk;
}
auto *c = new Server_Counter(newCounterId(), nameFromStdString(cmd.counter_name()), cmd.counter_color(),
cmd.radius(), cmd.value());
Response::ResponseCode
Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
const QString counterName = nameFromStdString(cmd.counter_name());
const Response::ResponseCode authResult = evaluateCreateCounter(game->getGameStarted(), conceded, counterName);
if (authResult != Response::RespOk) {
return authResult;
}
auto *c = new Server_Counter(newCounterId(), counterName, cmd.counter_color(), cmd.radius(), cmd.value());
addCounter(c);
Event_CreateCounter event;
@ -482,17 +520,12 @@ Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContai
Response::ResponseCode
Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
if (!game->getGameStarted()) {
return Response::RespGameNotStarted;
}
if (conceded) {
return Response::RespContextError;
}
const int counterId = cmd.counter_id();
Server_Counter *c = counters.value(counterId, nullptr);
if (!c) {
return Response::RespNameNotFound;
const Response::ResponseCode authResult = evaluateModifyCounter(game->getGameStarted(), conceded, c);
if (authResult != Response::RespOk) {
return authResult;
}
bool didChange = c->setCount(cmd.value());
@ -507,20 +540,31 @@ Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &
}
Response::ResponseCode
Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
Server_Player::evaluateDelCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter)
{
if (!game->getGameStarted()) {
if (!gameStarted) {
return Response::RespGameNotStarted;
}
if (conceded) {
if (playerConceded) {
return Response::RespContextError;
}
const int counterId = cmd.counter_id();
Server_Counter *counter = counters.value(counterId, nullptr);
if (!counter) {
return Response::RespNameNotFound;
}
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
const int counterId = cmd.counter_id();
Server_Counter *counter = counters.value(counterId, nullptr);
const Response::ResponseCode authResult = evaluateDelCounter(game->getGameStarted(), conceded, counter);
if (authResult != Response::RespOk) {
return authResult;
}
counters.remove(counterId);
delete counter;
@ -531,6 +575,187 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &
return Response::RespOk;
}
// Cast count validation and command handlers
Response::ResponseCode Server_Player::evaluateCreateCastCount(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
int index,
bool exists,
bool predecessorExists)
{
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::RespContextError;
}
if (index > 1 && !predecessorExists) {
return Response::RespContextError;
}
return Response::RespOk;
}
Response::ResponseCode Server_Player::evaluateDeleteCastCount(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
int index,
bool exists,
int value,
bool successorExists)
{
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;
}
if (value != 0) {
return Response::RespContextError;
}
if (successorExists) {
return Response::RespContextError;
}
return Response::RespOk;
}
Response::ResponseCode Server_Player::evaluateModifyCastCount(bool gameStarted,
bool playerConceded,
bool commandZoneEnabled,
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;
}
Response::ResponseCode
Server_Player::cmdCreateCastCount(const Command_CreateCastCount &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{
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) {
return authResult;
}
castCounts.insert(index, 0);
Event_CreateCastCount event;
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);
}
return Response::RespOk;
}
Response::ResponseCode
Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/)
{
@ -604,4 +829,10 @@ void Server_Player::getInfo(ServerInfo_Player *info,
for (Server_Counter *counter : counters) {
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
private:
QMap<int, Server_Counter *> counters;
QMap<int, int> castCounts; // index (1-5) -> value
QList<int> lastDrawList;
public:
@ -21,9 +22,41 @@ public:
{
return counters;
}
const QMap<int, int> &getCastCounts() const
{
return castCounts;
}
int newCounterId() const;
void addCounter(Server_Counter *counter);
static Response::ResponseCode
evaluateDelCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter);
static Response::ResponseCode
evaluateModifyCounter(bool gameStarted, bool playerConceded, const Server_Counter *counter);
static Response::ResponseCode
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 clearZones() override;
@ -66,6 +99,16 @@ public:
ResponseContainer &rc,
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,
Server_AbstractParticipant *playerWhosAsking,
bool omniscient,

View file

@ -899,10 +899,11 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
// When server doesn't permit registered users to exist, do not honor only-reg setting
bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers());
auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()),
cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers,
cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(),
cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room);
auto *game =
new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()),
cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, cmd.spectators_allowed(),
cmd.spectators_need_password(), cmd.spectators_can_talk(), cmd.spectators_see_everything(),
startingLifeTotal, shareDecklistsOnLoad, cmd.enable_command_zone(), room);
game->addPlayer(this, rc, asSpectator, asJudge, false);
room->addGame(game);

View file

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

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

@ -175,6 +175,26 @@ message GameCommand {
/// Server: Server_Player::cmdReverseTurn
/// Client: reflected via subsequent turn events
REVERSE_TURN = 1034;
/// Create a cast count at the specified index.
/// Server: Server_Player::cmdCreateCastCount
/// Client: PlayerEventHandler::eventCreateCastCount
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;

View file

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

View file

@ -69,6 +69,9 @@ message Command_CreateGame {
// share decklists with all players when selected
optional bool share_decklists_on_load = 14;
// enable command zone for Commander format
optional bool enable_command_zone = 15;
}
message Command_JoinGame {

View file

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

View file

@ -7,4 +7,5 @@ message ServerInfo_Counter {
optional color counter_color = 3;
optional sint32 radius = 4;
optional sint32 count = 5;
optional bool active = 6 [default = true];
}

View file

@ -48,6 +48,9 @@ message ServerInfo_Game {
// decklists are sent to all players when loaded
optional bool share_decklists_on_load = 15;
// command zone is enabled for Commander format
optional bool enable_command_zone = 16;
// the current player count
optional uint32 player_count = 30;

View file

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

View file

@ -94,6 +94,17 @@ bool GameFiltersSettings::isHideOpenDecklistGames() const
return previous == QVariant() ? false : previous.toBool();
}
void GameFiltersSettings::setHideCommandZoneGames(bool hide)
{
setValue(hide, "hide_command_zone_games");
}
bool GameFiltersSettings::isHideCommandZoneGames() const
{
QVariant previous = getValue("hide_command_zone_games");
return previous == QVariant() ? false : previous.toBool();
}
void GameFiltersSettings::setGameNameFilter(QString gameName)
{
setValue(gameName, "gameNameFilter");

View file

@ -23,6 +23,7 @@ public:
bool isHideIgnoredUserGames() const;
bool isHideNotBuddyCreatedGames() const;
bool isHideOpenDecklistGames() const;
bool isHideCommandZoneGames() const;
QString getGameNameFilter() const;
QStringList getCreatorNameFilters() const;
int getMinPlayers() const;
@ -37,6 +38,7 @@ public:
void setHideBuddiesOnlyGames(bool hide);
void setHideIgnoredUserGames(bool hide);
void setHideOpenDecklistGames(bool hide);
void setHideCommandZoneGames(bool hide);
void setHideFullGames(bool hide);
void setHideGamesThatStarted(bool hide);
void setHidePasswordProtectedGames(bool hide);

View file

@ -70,6 +70,11 @@ bool GameSettings::getRememberGameSettings() const
return getValue("rememberGameSettings", "game", QString(), true).toBool();
}
bool GameSettings::getEnableCommandZone() const
{
return getValue("enablecommandzone", "game").toBool();
}
bool GameSettings::getLocalGameRememberSettings() const
{
return getValue("rememberSettings", "localgameoptions").toBool();
@ -85,6 +90,11 @@ int GameSettings::getLocalGameStartingLifeTotal() const
return getValue("startingLifeTotal", "localgameoptions", QString(), 20).toInt();
}
bool GameSettings::getLocalGameEnableCommandZone() const
{
return getValue("enablecommandzone", "localgameoptions").toBool();
}
void GameSettings::setGameDescription(const QString &_gameDescription)
{
setValue(_gameDescription, "gameDescription", "game");
@ -150,6 +160,11 @@ void GameSettings::setRememberGameSettings(bool _rememberGameSettings)
setValue(_rememberGameSettings, "rememberGameSettings", "game");
}
void GameSettings::setEnableCommandZone(bool _enableCommandZone)
{
setValue(_enableCommandZone, "enablecommandzone", "game");
}
void GameSettings::setLocalGameRememberSettings(bool value)
{
setValue(value, "rememberSettings", "localgameoptions");
@ -164,3 +179,8 @@ void GameSettings::setLocalGameStartingLifeTotal(int value)
{
setValue(value, "startingLifeTotal", "localgameoptions");
}
void GameSettings::setLocalGameEnableCommandZone(bool value)
{
setValue(value, "enablecommandzone", "localgameoptions");
}

View file

@ -24,9 +24,11 @@ public:
[[nodiscard]] int getDefaultStartingLifeTotal() const override;
[[nodiscard]] bool getShareDecklistsOnLoad() const override;
[[nodiscard]] bool getRememberGameSettings() const override;
[[nodiscard]] bool getEnableCommandZone() const override;
[[nodiscard]] bool getLocalGameRememberSettings() const override;
[[nodiscard]] int getLocalGameMaxPlayers() const override;
[[nodiscard]] int getLocalGameStartingLifeTotal() const override;
[[nodiscard]] bool getLocalGameEnableCommandZone() const override;
void setGameDescription(const QString &_gameDescription);
void setMaxPlayers(int _maxPlayers);
@ -41,9 +43,11 @@ public:
void setDefaultStartingLifeTotal(int _defaultStartingLifeTotal);
void setShareDecklistsOnLoad(bool _shareDecklistsOnLoad);
void setRememberGameSettings(bool _rememberGameSettings);
void setEnableCommandZone(bool _enableCommandZone);
void setLocalGameRememberSettings(bool value);
void setLocalGameMaxPlayers(int value);
void setLocalGameStartingLifeTotal(int value);
void setLocalGameEnableCommandZone(bool value);
public:
explicit GameSettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -18,6 +18,7 @@ set(UTILITY_HEADERS
libcockatrice/utility/string_limits.h
libcockatrice/utility/dice_limits.h
libcockatrice/utility/counter_limits.h
libcockatrice/utility/counter_ids.h
libcockatrice/utility/clamped_arithmetic.h
libcockatrice/utility/zone_names.h
libcockatrice/utility/days_years_between.h

View file

@ -0,0 +1,49 @@
/**
* @file counter_ids.h
* @ingroup Utility
* @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
#define COCKATRICE_COUNTER_IDS_H
#include <QString>
/**
* @namespace CounterIds
* @brief Reserved counter ID ranges.
*/
namespace CounterIds
{
constexpr int FirstUserId = 8; ///< First ID available for user-created counters (0-7 are built-in)
} // namespace CounterIds
/**
* @namespace CastCountIds
* @brief Cast count index range and utilities.
*
* Cast counts use a separate 1-based index space (1-5), not sharing IDs with regular counters.
*/
namespace CastCountIds
{
constexpr int MinIndex = 1; ///< Minimum valid cast count index
constexpr int MaxIndex = 5; ///< Maximum valid cast count index
constexpr int Count = MaxIndex; ///< Total number of cast count slots
/** @brief Returns true if the index is in the valid cast count range [1, 5]. */
inline bool isValidIndex(int index)
{
return index >= MinIndex && index <= MaxIndex;
}
/** @brief Returns the internal name for a cast count at the given index (e.g. "cast_count_1"). */
inline QString nameForIndex(int index)
{
return QString("cast_count_%1").arg(index);
}
} // namespace CastCountIds
#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
* 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.
*
* The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit.

View file

@ -14,6 +14,9 @@ constexpr const char *DECK = "deck";
constexpr const char *SIDEBOARD = "sb";
constexpr const char *STACK = "stack";
// Command zone (Commander format)
constexpr const char *COMMAND = "command";
} // namespace ZoneNames
#endif // ZONE_NAMES_H

View file

@ -90,6 +90,7 @@ target_link_libraries(
add_subdirectory(card_zone_algorithms)
add_subdirectory(carddatabase)
add_subdirectory(command_zone_tests)
add_subdirectory(loading_from_clipboard)
add_subdirectory(movecard_tests)
add_subdirectory(oracle)

View file

@ -0,0 +1,50 @@
add_executable(new_counter_id_test new_counter_id_test.cpp)
if(NOT GTEST_FOUND)
add_dependencies(new_counter_id_test gtest)
endif()
target_link_libraries(
new_counter_id_test
PRIVATE libcockatrice_network_server_remote
PRIVATE libcockatrice_rng
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME new_counter_id_test COMMAND new_counter_id_test)
add_executable(counter_command_auth_test counter_command_auth_test.cpp)
if(NOT GTEST_FOUND)
add_dependencies(counter_command_auth_test gtest)
endif()
target_link_libraries(
counter_command_auth_test
PRIVATE libcockatrice_network_server_remote
PRIVATE libcockatrice_rng
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME counter_command_auth_test COMMAND counter_command_auth_test)
add_executable(setup_zones_command_zone_test setup_zones_command_zone_test.cpp)
if(NOT GTEST_FOUND)
add_dependencies(setup_zones_command_zone_test gtest)
endif()
target_link_libraries(
setup_zones_command_zone_test
PRIVATE libcockatrice_network_server_remote
PRIVATE libcockatrice_rng
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME setup_zones_command_zone_test COMMAND setup_zones_command_zone_test)

View file

@ -0,0 +1,220 @@
/** @file counter_command_auth_test.cpp
* @brief Tests for Server_Player counter-command authorization helpers.
* @ingroup Tests
*/
#include "game/server_counter.h"
#include "game/server_player.h"
#include <gtest/gtest.h>
#include <libcockatrice/protocol/pb/color.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/counter_ids.h>
RNG_Abstract *rng = nullptr; // required by linked server code
namespace
{
constexpr int UserCounterId = CounterIds::FirstUserId;
Server_Counter makeCounter(int id, int count)
{
return Server_Counter(id, "c", color(), 20, count);
}
} // namespace
TEST(EvaluateDelCounter, RejectsWhenGameNotStarted)
{
Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateDelCounter(/*gameStarted=*/false, /*playerConceded=*/false, &counter),
Response::RespGameNotStarted);
}
TEST(EvaluateDelCounter, RejectsWhenPlayerConceded)
{
Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateDelCounter(/*gameStarted=*/true, /*playerConceded=*/true, &counter),
Response::RespContextError);
}
TEST(EvaluateDelCounter, RejectsMissingCounter)
{
EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, nullptr), Response::RespNameNotFound);
}
TEST(EvaluateDelCounter, AllowsDeletingUserCounter)
{
Server_Counter counter = makeCounter(UserCounterId, 7);
EXPECT_EQ(Server_Player::evaluateDelCounter(true, false, &counter), Response::RespOk);
}
TEST(EvaluateCreateCounter, RejectsWhenGameNotStarted)
{
EXPECT_EQ(Server_Player::evaluateCreateCounter(/*gameStarted=*/false, /*playerConceded=*/false, "test"),
Response::RespGameNotStarted);
}
TEST(EvaluateCreateCounter, RejectsWhenPlayerConceded)
{
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, /*playerConceded=*/true, "test"), Response::RespContextError);
}
TEST(EvaluateCreateCounter, AllowsUserCounterName)
{
EXPECT_EQ(Server_Player::evaluateCreateCounter(true, false, "poison"), Response::RespOk);
}
TEST(EvaluateModifyCounter, RejectsWhenGameNotStarted)
{
Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateModifyCounter(/*gameStarted=*/false, /*playerConceded=*/false, &counter),
Response::RespGameNotStarted);
}
TEST(EvaluateModifyCounter, RejectsWhenPlayerConceded)
{
Server_Counter counter = makeCounter(UserCounterId, 0);
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, /*playerConceded=*/true, &counter),
Response::RespContextError);
}
TEST(EvaluateModifyCounter, RejectsMissingCounter)
{
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, nullptr), Response::RespNameNotFound);
}
TEST(EvaluateModifyCounter, AllowsUserCounter)
{
Server_Counter counter = makeCounter(UserCounterId, 5);
EXPECT_EQ(Server_Player::evaluateModifyCounter(true, false, &counter), Response::RespOk);
}
// Cast count evaluation tests
TEST(EvaluateCreateCastCount, RejectsWhenGameNotStarted)
{
EXPECT_EQ(Server_Player::evaluateCreateCastCount(/*gameStarted=*/false, /*playerConceded=*/false,
/*commandZoneEnabled=*/true, 1, /*exists=*/false,
/*predecessorExists=*/true),
Response::RespGameNotStarted);
}
TEST(EvaluateCreateCastCount, RejectsWhenPlayerConceded)
{
EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, /*playerConceded=*/true, true, 1, false, true),
Response::RespContextError);
}
TEST(EvaluateCreateCastCount, RejectsWhenCommandZoneDisabled)
{
EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, /*commandZoneEnabled=*/false, 1, false, true),
Response::RespContextError);
}
TEST(EvaluateCreateCastCount, RejectsInvalidIndex)
{
EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, true, 0, false, false), Response::RespContextError);
EXPECT_EQ(Server_Player::evaluateCreateCastCount(true, false, true, 6, false, false), Response::RespContextError);
}
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);
}
TEST(EvaluateDeleteCastCount, RejectsWhenGameNotStarted)
{
EXPECT_EQ(Server_Player::evaluateDeleteCastCount(/*gameStarted=*/false, /*playerConceded=*/false,
/*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);
}
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)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -0,0 +1,72 @@
/** @file new_counter_id_test.cpp
* @brief Tests for Server_Player::newCounterId() id allocation.
* @ingroup Tests
*/
#include "../movecard_tests/server_test_helpers.h"
#include "game/server_counter.h"
#include "game/server_game.h"
#include "game/server_player.h"
#include "server_room.h"
#include <gtest/gtest.h>
#include <libcockatrice/protocol/pb/color.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/counter_ids.h>
RNG_Abstract *rng = nullptr; // required by linked server code
namespace
{
struct PlayerFixture
{
ServerInfo_User user;
FakeServer server;
Server_Room room{0, 0, "", "", "", "", false, "", {}, &server};
Server_Game game{user, 1, "", "", 2, QList<int>(), false, false,
false, false, false, false, 20, false, false, &room};
Server_Player player{&game, 1, user, false, nullptr};
~PlayerFixture()
{
player.clearZones(); // owns and deletes any counters added during the test
}
};
} // namespace
TEST(NewCounterId, ReturnsFirstUserIdWhenNoCounters)
{
PlayerFixture f;
EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId);
}
TEST(NewCounterId, SkipsReservedRangeWhenOnlyReservedCountersExist)
{
PlayerFixture f;
f.player.addCounter(new Server_Counter(3, "g", color(), 20, 0));
f.player.addCounter(new Server_Counter(5, "h", color(), 20, 0));
EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId);
}
TEST(NewCounterId, ReturnsNextIdAboveHighestUserCounter)
{
PlayerFixture f;
f.player.addCounter(new Server_Counter(CounterIds::FirstUserId, "a", color(), 20, 0));
f.player.addCounter(new Server_Counter(CounterIds::FirstUserId + 2, "b", color(), 20, 0));
EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId + 3);
}
TEST(NewCounterId, IgnoresReservedCountersWhenUserCountersPresent)
{
PlayerFixture f;
f.player.addCounter(new Server_Counter(5, "r", color(), 20, 0)); // reserved range
f.player.addCounter(new Server_Counter(CounterIds::FirstUserId, "user", color(), 20, 0)); // user range
EXPECT_EQ(f.player.newCounterId(), CounterIds::FirstUserId + 1);
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -0,0 +1,116 @@
/** @file setup_zones_command_zone_test.cpp
* @brief Tests for the command-zone branch of Server_Player::setupZones().
* @ingroup Tests
*/
#include "../movecard_tests/server_test_helpers.h"
#include "game/server_counter.h"
#include "game/server_game.h"
#include "game/server_player.h"
#include "server_room.h"
#include <gtest/gtest.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/protocol/pb/color.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/utility/counter_ids.h>
#include <libcockatrice/utility/counter_limits.h>
#include <libcockatrice/utility/zone_names.h>
RNG_Abstract *rng = nullptr; // required by linked server code
namespace
{
/**
* @brief A Server_Player whose protected deck member is populated with an empty deck.
*
* setupZones() reads deck->getCardNodes(); the base member is null by default, so this
* subclass injects an empty deck. An empty deck makes the card-loading loops no-ops,
* leaving only the zone/counter setup under test. The base destructor does not free deck
* (only prepareDestroy() does, which the test never calls), so this subclass deletes it.
*/
struct CommandZoneTestPlayer : Server_Player
{
CommandZoneTestPlayer(Server_Game *_game, const ServerInfo_User &_userInfo)
: Server_Player(_game, 1, _userInfo, false, nullptr)
{
deck = new DeckList();
}
~CommandZoneTestPlayer() override
{
delete deck;
deck = nullptr;
}
};
/**
* @brief Builds a Server_Player on a minimal fake game whose command zone is toggled by the
* constructor argument, then runs setupZones() so getCastCounts() reflects the result.
*/
struct SetupFixture
{
ServerInfo_User user;
FakeServer server;
Server_Room room{0, 0, "", "", "", "", false, "", {}, &server};
Server_Game game;
CommandZoneTestPlayer player;
explicit SetupFixture(bool commandZoneEnabled)
: game(user,
1,
"",
"",
2,
QList<int>(),
false,
false,
false,
false,
false,
false,
20,
false,
commandZoneEnabled,
&room),
player(&game, user)
{
player.setupZones();
}
~SetupFixture()
{
player.clearZones(); // owns and deletes the counters created by setupZones()
}
};
} // namespace
TEST(SetupZonesCommandZone, CreatesFirstCastCountWhenEnabled)
{
SetupFixture f(true);
const QMap<int, int> &castCounts = f.player.getCastCounts();
EXPECT_TRUE(f.player.getZones().contains(ZoneNames::COMMAND));
EXPECT_EQ(castCounts.size(), 1);
EXPECT_TRUE(castCounts.contains(1));
EXPECT_EQ(castCounts.value(1), 0);
}
TEST(SetupZonesCommandZone, NoCastCountsWhenDisabled)
{
SetupFixture f(false);
const QMap<int, int> &castCounts = f.player.getCastCounts();
EXPECT_FALSE(f.player.getZones().contains(ZoneNames::COMMAND));
EXPECT_TRUE(castCounts.isEmpty());
EXPECT_TRUE(f.player.getCounters().contains(0));
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -22,7 +22,8 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest)
// instantiate a fake server instance
FakeServer server;
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
Server_Game game(user, 1, "", "", 2, QList<int>(), false, false, false, false, false, false, 20, false, &room);
Server_Game game(user, 1, "", "", 2, QList<int>(), false, false, false, false, false, false, 20, false, false,
&room);
Server_AbstractPlayer player(&game, 1, user, false, nullptr);
Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone);
Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone);

View file

@ -5,6 +5,7 @@
#include <gtest/gtest.h>
#include <libcockatrice/network/server/remote/game/server_counter.h>
#include <libcockatrice/utility/counter_limits.h>
#include <limits>
TEST(ServerCounter, IncrementDoesNotOverflow)
@ -79,6 +80,47 @@ TEST(ServerCounter, MixedExtremesDoNotClamp)
EXPECT_EQ(c.getCount(), -1);
}
TEST(ServerCounter, SetCountClampsToCustomBounds)
{
Server_Counter c(1, "test", color(), 10, 50, 0, 100);
EXPECT_TRUE(c.setCount(150));
EXPECT_EQ(c.getCount(), 100);
EXPECT_TRUE(c.setCount(-10));
EXPECT_EQ(c.getCount(), 0);
}
TEST(ServerCounter, IncrementClampsToCustomBounds)
{
Server_Counter c(1, "test", color(), 10, 50, 0, 100);
EXPECT_TRUE(c.incrementCount(100));
EXPECT_EQ(c.getCount(), 100);
EXPECT_FALSE(c.incrementCount(1));
EXPECT_EQ(c.getCount(), 100);
EXPECT_TRUE(c.incrementCount(-200));
EXPECT_EQ(c.getCount(), 0);
EXPECT_FALSE(c.incrementCount(-1));
EXPECT_EQ(c.getCount(), 0);
}
TEST(ServerCounter, CustomBoundsForCastCount)
{
Server_Counter castCount(1, "cast_count", color(), 20, 0, 0, MAX_COUNTER_VALUE);
EXPECT_TRUE(castCount.setCount(1000));
EXPECT_EQ(castCount.getCount(), MAX_COUNTER_VALUE);
EXPECT_TRUE(castCount.setCount(-5));
EXPECT_EQ(castCount.getCount(), 0);
}
TEST(ServerCounter, SetActiveReportsChangeOnce)
{
Server_Counter c(1, "test", color(), 10, 0);
EXPECT_TRUE(c.isActive());
EXPECT_TRUE(c.setActive(false));
EXPECT_FALSE(c.isActive());
EXPECT_FALSE(c.setActive(false));
EXPECT_TRUE(c.setActive(true));
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);

View file

@ -115,6 +115,18 @@ TEST_F(SettingsDefaultsTest, Game_LocalGameRememberSettings_Default)
ASSERT_EQ(s.getLocalGameRememberSettings(), false);
}
TEST_F(SettingsDefaultsTest, Game_EnableCommandZone_Default)
{
GameSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getEnableCommandZone(), false);
}
TEST_F(SettingsDefaultsTest, Game_LocalGameEnableCommandZone_Default)
{
GameSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getLocalGameEnableCommandZone(), false);
}
// --- UpdatesSettings ---
TEST_F(SettingsDefaultsTest, Updates_CheckUpdatesOnStartup_Default)