diff --git a/Testing/Temporary/CTestCostData.txt b/Testing/Temporary/CTestCostData.txt new file mode 100644 index 000000000..ed97d539c --- /dev/null +++ b/Testing/Temporary/CTestCostData.txt @@ -0,0 +1 @@ +--- diff --git a/Testing/Temporary/LastTest.log b/Testing/Temporary/LastTest.log new file mode 100644 index 000000000..6f9e41de7 --- /dev/null +++ b/Testing/Temporary/LastTest.log @@ -0,0 +1,3 @@ +Start testing: Aug 10 22:25 EDT +---------------------------------------------------------- +End testing: Aug 10 22:25 EDT diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 9fd05ae01..5c5373848 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -67,6 +67,7 @@ set(cockatrice_SOURCES src/game_graphics/board/card_item.cpp src/game/board/card_list.cpp src/game/board/card_state.cpp + src/game_graphics/board/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 diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 95155b8d1..4f4984235 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -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)}, diff --git a/cockatrice/src/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp index 6da18b662..2ccdb49df 100644 --- a/cockatrice/src/game/board/counter_state.cpp +++ b/cockatrice/src/game/board/counter_state.cpp @@ -2,15 +2,22 @@ #include -CounterState::CounterState(int id, const QString &name, const QColor &color, int radius, int value, QObject *parent) - : QObject(parent), id(id), name(name), color(color), radius(radius), value(value) +CounterState::CounterState(int _id, + const QString &_name, + const QColor &_color, + int _radius, + int _value, + bool _active, + QObject *parent) + : QObject(parent), id(_id), name(_name), color(_color), radius(_radius), value(_value), active(_active) { } CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject *parent) { return new CounterState(counter.id(), QString::fromStdString(counter.name()), - convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent); + convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), + counter.active(), parent); } void CounterState::setValue(int newValue) @@ -21,4 +28,13 @@ void CounterState::setValue(int newValue) int old = value; value = newValue; emit valueChanged(old, newValue); -} \ No newline at end of file +} + +void CounterState::setActive(bool newActive) +{ + if (newActive == active) { + return; + } + active = newActive; + emit activeChanged(newActive); +} diff --git a/cockatrice/src/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h index 0f2f16b55..003a7f8e9 100644 --- a/cockatrice/src/game/board/counter_state.h +++ b/cockatrice/src/game/board/counter_state.h @@ -10,7 +10,13 @@ class CounterState : public QObject { Q_OBJECT public: - CounterState(int id, const QString &name, const QColor &color, int radius, int value, QObject *parent = nullptr); + CounterState(int _id, + const QString &_name, + const QColor &_color, + int _radius, + int _value, + bool _active = true, + QObject *parent = nullptr); static CounterState *fromProto(const ServerInfo_Counter &counter, QObject *parent = nullptr); @@ -34,11 +40,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 diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 67c3295d6..d9f416e08 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -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 #include #include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -27,9 +31,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -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 selectedCards, const bool faceDown) +{ + playSelectedCardsImpl(selectedCards, faceDown, nullptr); +} + +void PlayerActions::playSelectedCardsImpl( + QList selectedCards, + bool faceDown, + const std::function &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 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 selectedCards) +{ + playAndIncreaseCastCount(selectedCards, 1); +} + +void PlayerActions::actPlayAndIncrease2ndCastCount(QList selectedCards) +{ + playAndIncreaseCastCount(selectedCards, 2); +} + +void PlayerActions::playAndIncreaseCastCount(QList 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 selectedCards) { playSelectedCards(selectedCards, false); @@ -1925,6 +2033,18 @@ void PlayerActions::cardMenuAction(QList selectedCards, CardMenuActi commandList.append(cmd); break; } + case cmMoveToCommandZone: { + auto *cmd = new Command_MoveCard; + cmd->set_start_player_id(startPlayerId); + cmd->set_start_zone(startZone.toStdString()); + cmd->mutable_cards_to_move()->CopyFrom(idList); + cmd->set_target_player_id(player->getPlayerInfo()->getId()); + cmd->set_target_zone(ZoneNames::COMMAND); + cmd->set_x(0); + cmd->set_y(0); + commandList.append(cmd); + break; + } case cmMoveToTable: { // Each card needs its own command because table row, pt, and cipt vary per card for (const auto &card : cardList) { diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index fa4d54110..12b5ab2a6 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -126,6 +127,14 @@ public slots: void actPlay(QList selectedCards); void actPlayFacedown(QList selectedCards); + /** @brief Plays the selected card and increments the 1st cast count. */ + void actPlayAndIncrease1stCastCount(QList selectedCards); + /** @brief Plays the selected card and increments the 2nd cast count. */ + void actPlayAndIncrease2ndCastCount(QList 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 selectedCards); void actMoveTopCardToPlay(); @@ -219,6 +228,9 @@ public slots: void cardMenuAction(QList 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 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 selectedCards, + bool faceDown, + const std::function &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 selectedCards, int index); + void cmdSetTopCard(Command_MoveCard &cmd); void cmdSetBottomCard(Command_MoveCard &cmd); diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index bc48298f7..e04bc6723 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -30,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -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; diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index 48ad85e88..f0f53cd32 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -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); diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 485e2fc5c..e3dd7e86c 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -28,6 +28,7 @@ #include #include #include +#include PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent) : QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)), @@ -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 diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index a89cb6eed..86881d82b 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -11,6 +11,7 @@ #include "../../interface/widgets/menus/tearoff_menu.h" #include "../board/arrow_data.h" #include "../interface/deck_loader/loaded_deck.h" +#include "../zones/command_zone_logic.h" #include "../zones/hand_zone_logic.h" #include "../zones/pile_zone_logic.h" #include "../zones/stack_zone_logic.h" @@ -25,6 +26,7 @@ #include #include #include +#include #include 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(zones.value(ZoneNames::HAND)); } + /** @brief Returns the command zone logic, or nullptr if not present. */ + CommandZoneLogic *getCommandZone() const + { + return qobject_cast(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 &getCastCounts() const + { + return castCounts; + } + void setConceded(bool _conceded); bool getConceded() const { @@ -240,6 +272,7 @@ private: int zoneId; QMap zones; QMap counters; + QMap castCounts; // index (1-5) -> state bool dialogSemaphore; QList cardsToDelete; diff --git a/cockatrice/src/game/zones/card_zone_logic.cpp b/cockatrice/src/game/zones/card_zone_logic.cpp index 7e0585f4e..136f8bd72 100644 --- a/cockatrice/src/game/zones/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/card_zone_logic.cpp @@ -202,6 +202,9 @@ QString CardZoneLogic::getTranslatedName(bool theirOwn, GrammaticalCase gc) cons return (theirOwn ? tr("their graveyard", "nominative") : tr("%1's graveyard", "nominative").arg(ownerName)); } else if (name == ZoneNames::EXILE) { return (theirOwn ? tr("their exile", "nominative") : tr("%1's exile", "nominative").arg(ownerName)); + } else if (name == ZoneNames::COMMAND) { + return (theirOwn ? tr("their command zone", "nominative") + : tr("%1's command zone", "nominative").arg(ownerName)); } else if (name == ZoneNames::SIDEBOARD) { switch (gc) { case CaseLookAtZone: diff --git a/cockatrice/src/game/zones/command_zone_logic.cpp b/cockatrice/src/game/zones/command_zone_logic.cpp new file mode 100644 index 000000000..687ee49d1 --- /dev/null +++ b/cockatrice/src/game/zones/command_zone_logic.cpp @@ -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); +} diff --git a/cockatrice/src/game/zones/command_zone_logic.h b/cockatrice/src/game/zones/command_zone_logic.h new file mode 100644 index 000000000..8085537ab --- /dev/null +++ b/cockatrice/src/game/zones/command_zone_logic.h @@ -0,0 +1,51 @@ +/** + * @file command_zone_logic.h + * @ingroup GameLogicZones + * @brief Logic layer for the command zone, used for Commander format. + */ + +#ifndef COCKATRICE_COMMAND_ZONE_LOGIC_H +#define COCKATRICE_COMMAND_ZONE_LOGIC_H +#include "card_zone_logic.h" + +/** + * @class CommandZoneLogic + * @brief Logic layer for managing cards in the command zone. + * + * Handles data storage and card management for the command zone in Commander format. + * Supports ordered card insertion for drag-and-drop operations. + * + * @see CommandZone for the graphics layer + * @see CardZoneLogic + */ +class CommandZoneLogic : public CardZoneLogic +{ + Q_OBJECT +public: + /** + * @brief Constructs a CommandZoneLogic instance. + * @param _player The player who owns this zone + * @param _name Zone name (ZoneNames::COMMAND) + * @param _hasCardAttr Whether cards in this zone have attributes + * @param _isShufflable Whether the zone can be shuffled + * @param _contentsKnown Whether the zone contents are public knowledge + * @param parent Parent QObject + */ + CommandZoneLogic(PlayerLogic *_player, + const QString &_name, + bool _hasCardAttr, + bool _isShufflable, + bool _contentsKnown, + QObject *parent = nullptr); + +protected: + /** + * @brief Adds a card at position x (y ignored). Appends if x is -1 or out of range. + * @param card Card to add + * @param x Insertion index, or -1 to append + * @param y Unused + */ + void addCardImpl(CardItem *card, int x, int y) override; +}; + +#endif // COCKATRICE_COMMAND_ZONE_LOGIC_H diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp index a20fb1b3c..e9745b7a6 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -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(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; } diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h index b319a722d..f4d31ba79 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -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 diff --git a/cockatrice/src/game_graphics/board/cast_count_widget.cpp b/cockatrice/src/game_graphics/board/cast_count_widget.cpp new file mode 100644 index 000000000..91198b67e --- /dev/null +++ b/cockatrice/src/game_graphics/board/cast_count_widget.cpp @@ -0,0 +1,52 @@ +#include "cast_count_widget.h" + +#include "../../game/board/counter_state.h" +#include "translate_counter_name.h" + +#include +#include + +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(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)); +} diff --git a/cockatrice/src/game_graphics/board/cast_count_widget.h b/cockatrice/src/game_graphics/board/cast_count_widget.h new file mode 100644 index 000000000..cac266137 --- /dev/null +++ b/cockatrice/src/game_graphics/board/cast_count_widget.h @@ -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 diff --git a/cockatrice/src/game_graphics/board/translate_counter_name.cpp b/cockatrice/src/game_graphics/board/translate_counter_name.cpp index 4f1c97657..1178aea63 100644 --- a/cockatrice/src/game_graphics/board/translate_counter_name.cpp +++ b/cockatrice/src/game_graphics/board/translate_counter_name.cpp @@ -8,4 +8,9 @@ const QMap TranslateCounterName::translated = { {"r", QT_TRANSLATE_NOOP("TranslateCounterName", "Red")}, {"g", QT_TRANSLATE_NOOP("TranslateCounterName", "Green")}, {"x", QT_TRANSLATE_NOOP("TranslateCounterName", "Colorless")}, - {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}}; + {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}, + {"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")}}; diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp index ccd903b04..c2d83345a 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -80,6 +81,8 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position fromStr = tr(" from sideboard"); } else if (zoneName == ZoneNames::STACK) { fromStr = tr(" from the stack"); + } else if (zoneName == ZoneNames::COMMAND) { + fromStr = tr(" from the command zone"); } else { fromStr = tr(" from custom zone '%1'").arg(zoneName); } @@ -344,6 +347,8 @@ void MessageLogWidget::logMoveCard(PlayerLogic *player, } else { finalStr = tr("%1 plays %2%3."); } + } else if (targetZoneName == ZoneNames::COMMAND) { + finalStr = tr("%1 moves %2%3 to the command zone."); } else { fourthArg = targetZoneName; if (card->getFaceDown()) { @@ -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"); } diff --git a/cockatrice/src/game_graphics/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h index a145d358d..7d02e3105 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.h +++ b/cockatrice/src/game_graphics/log/message_log_widget.h @@ -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); diff --git a/cockatrice/src/game_graphics/player/card_menu_action_type.h b/cockatrice/src/game_graphics/player/card_menu_action_type.h index 4cae22716..f0fc1bd37 100644 --- a/cockatrice/src/game_graphics/player/card_menu_action_type.h +++ b/cockatrice/src/game_graphics/player/card_menu_action_type.h @@ -22,7 +22,8 @@ enum CardMenuActionType cmMoveToHand, cmMoveToGraveyard, cmMoveToExile, - cmMoveToTable + cmMoveToTable, + cmMoveToCommandZone }; #endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp index 75ca1b28b..687b1557c 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp @@ -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 #include #include +#include #include /** @@ -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")); diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.h b/cockatrice/src/game_graphics/player/menu/card_menu.h index d67ef3876..dbaf1a7bf 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.h +++ b/cockatrice/src/game_graphics/player/menu/card_menu.h @@ -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; diff --git a/cockatrice/src/game_graphics/player/menu/command_zone_menu.cpp b/cockatrice/src/game_graphics/player/menu/command_zone_menu.cpp new file mode 100644 index 000000000..527d4d2b2 --- /dev/null +++ b/cockatrice/src/game_graphics/player/menu/command_zone_menu.cpp @@ -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 +#include + +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 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()); + } +} diff --git a/cockatrice/src/game_graphics/player/menu/command_zone_menu.h b/cockatrice/src/game_graphics/player/menu/command_zone_menu.h new file mode 100644 index 000000000..09b7fceca --- /dev/null +++ b/cockatrice/src/game_graphics/player/menu/command_zone_menu.h @@ -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 +#include +#include + +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 aIncrease{}; + std::array aDecrease{}; + std::array 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 diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp index 632e0ddda..d0f0b1dfe 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp @@ -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")); } diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.h b/cockatrice/src/game_graphics/player/menu/move_menu.h index 150bdbd3c..2acf98e67 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.h +++ b/cockatrice/src/game_graphics/player/menu/move_menu.h @@ -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 diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp index 7c90211df..c9929abf8 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp @@ -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(player, playerMenu); + + if (player->getLogic()->hasCommandZone()) { + commandZoneMenu = addManagedMenu(player, playerMenu); + } else { + commandZoneMenu = nullptr; + } + customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); @@ -40,6 +48,7 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_ utilityMenu = createManagedComponent(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); + } } } diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h index a6e89fab0..c32b785d0 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.h +++ b/cockatrice/src/game_graphics/player/menu/player_menu.h @@ -8,6 +8,8 @@ #define COCKATRICE_PLAYER_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "../../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 diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index d443853ce..e63f7f5b4 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -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 #include +#include 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(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 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)); +} diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h index d02234ded..b19ae7a0d 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -12,6 +12,7 @@ #include +class CommandZone; class HandZone; class PileZone; class PlayerDialogs; @@ -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 counterWidgets; + QMap castCountWidgets; QMap 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(); diff --git a/cockatrice/src/game_graphics/z_values.h b/cockatrice/src/game_graphics/z_values.h index c6e7f2c8a..c07d3d03e 100644 --- a/cockatrice/src/game_graphics/z_values.h +++ b/cockatrice/src/game_graphics/z_values.h @@ -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); diff --git a/cockatrice/src/game_graphics/zones/command_zone.cpp b/cockatrice/src/game_graphics/zones/command_zone.cpp new file mode 100644 index 000000000..7db27e13f --- /dev/null +++ b/cockatrice/src/game_graphics/zones/command_zone.cpp @@ -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 +#include +#include +#include + +CommandZone::CommandZone(CommandZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent) + : SelectZone(_logic, parent), zoneHeight(_zoneHeight) +{ + connect(themeManager, &ThemeManager::themeChanged, this, &CommandZone::updateBg); + updateBg(); + setCacheMode(DeviceCoordinateCache); + setupClipContainer(ZValues::CARD_BASE); +} + +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(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 &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(y)); +} + +void CommandZone::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + toggleMinimized(); + event->accept(); + } else { + SelectZone::mouseDoubleClickEvent(event); + } +} diff --git a/cockatrice/src/game_graphics/zones/command_zone.h b/cockatrice/src/game_graphics/zones/command_zone.h new file mode 100644 index 000000000..8b309669b --- /dev/null +++ b/cockatrice/src/game_graphics/zones/command_zone.h @@ -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 + +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 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 &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 diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp index c58c41b92..8e9228f9f 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.cpp +++ b/cockatrice/src/game_graphics/zones/select_zone.cpp @@ -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() diff --git a/cockatrice/src/game_graphics/zones/select_zone.h b/cockatrice/src/game_graphics/zones/select_zone.h index 7408f29b6..7b52fd5cb 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.h +++ b/cockatrice/src/game_graphics/zones/select_zone.h @@ -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; diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index 184f96d62..c22822421 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -32,6 +32,10 @@ QRectF StackZone::boundingRect() const void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId()); + + QPointF scenePos = mapToScene(QPointF(0, 0)); + painter->setBrushOrigin(-scenePos); + painter->fillRect(boundingRect(), brush); } @@ -43,19 +47,7 @@ void StackZone::handleDropEvent(const QList &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(cards.size()); - } + int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE); Command_MoveCard cmd; cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 518a97bc6..7e2162f92 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -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 {}; diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index 861ab838b..8b6c568a2 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -38,7 +38,8 @@ public: Stack, Table, Player, - MaxRole = Player, + Command, + MaxRole = Command, }; private: diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 59f3e033d..d736bcf26 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -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 &_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 QMapsetEnabled(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 QMapsetChecked(gameInfo.spectators_can_chat()); spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient()); shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load()); + enableCommandZoneCheckBox->setChecked(gameInfo.enable_command_zone()); QSet 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 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 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); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h index 61925286d..4bb5eeb64 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h @@ -48,6 +48,7 @@ private: QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox, *spectatorsSeeEverythingCheckBox, *createGameAsJudgeCheckBox, *createGameAsSpectatorCheckBox; QCheckBox *shareDecklistsOnLoadCheckBox; + QCheckBox *enableCommandZoneCheckBox; QDialogButtonBox *buttonBox; QPushButton *clearButton; QCheckBox *rememberGameSettings; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp index 4e45f2c25..f1e6f7616 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp @@ -45,6 +45,9 @@ DlgFilterGames::DlgFilterGames(const QMap &_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 &_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(), diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h index 447f9b16c..1da3a32ef 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h @@ -34,6 +34,7 @@ private: QCheckBox *hideIgnoredUserGames; QCheckBox *hideNotBuddyCreatedGames; QCheckBox *hideOpenDecklistGames; + QCheckBox *hideCommandZoneGames; QLineEdit *gameNameFilterEdit; QLineEdit *creatorNameFilterEdit; QMap gameTypeFilterCheckBoxes; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp index eca0ec658..e62b9ca04 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp @@ -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(), }; } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h index 4307581a4..d52257ec6 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.h @@ -16,6 +16,7 @@ struct LocalGameOptions { int numberPlayers = 1; int startingLifeTotal = 20; + bool enableCommandZone = false; }; class QCheckBox; @@ -45,6 +46,7 @@ private: QLabel *startingLifeTotalLabel; QSpinBox *startingLifeTotalEdit; + QCheckBox *enableCommandZoneCheckBox; QCheckBox *rememberSettingsCheckBox; QDialogButtonBox *buttonBox; diff --git a/cockatrice/src/interface/widgets/server/game_filter_configs.h b/cockatrice/src/interface/widgets/server/game_filter_configs.h index 0ece7e00c..1b2cfe91d 100644 --- a/cockatrice/src/interface/widgets/server/game_filter_configs.h +++ b/cockatrice/src/interface/widgets/server/game_filter_configs.h @@ -18,6 +18,7 @@ struct GameFilterConfigs bool hidePasswordProtectedGames = false; bool hideNotBuddyCreatedGames = false; bool hideOpenDecklistGames = false; + bool hideCommandZoneGames = false; QString gameNameFilter = ""; QStringList creatorNameFilters = {}; QSet gameTypeFilter = {}; diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index 8e623d392..992a23d52 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -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 &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 &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; diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 44e188760..afbbfb7e8 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -183,6 +183,7 @@ void MainWindow::startLocalGame(const LocalGameOptions &options) Command_CreateGame createCommand; createCommand.set_max_players(static_cast(options.numberPlayers)); createCommand.set_starting_life_total(options.startingLifeTotal); + createCommand.set_enable_command_zone(options.enableCommandZone); mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0)); } diff --git a/cockatrice/themes/Fabric/zones/commandzone.png b/cockatrice/themes/Fabric/zones/commandzone.png new file mode 100644 index 000000000..e71f466e8 Binary files /dev/null and b/cockatrice/themes/Fabric/zones/commandzone.png differ diff --git a/cockatrice/themes/Leather/zones/commandzone.png b/cockatrice/themes/Leather/zones/commandzone.png new file mode 100644 index 000000000..8159dfaa0 Binary files /dev/null and b/cockatrice/themes/Leather/zones/commandzone.png differ diff --git a/cockatrice/themes/Plasma/zones/commandzone.png b/cockatrice/themes/Plasma/zones/commandzone.png new file mode 100644 index 000000000..115a0d613 Binary files /dev/null and b/cockatrice/themes/Plasma/zones/commandzone.png differ diff --git a/cockatrice/themes/VelvetMarble/zones/commandzone.jpg b/cockatrice/themes/VelvetMarble/zones/commandzone.jpg new file mode 100644 index 000000000..9fa90fb64 Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/commandzone.jpg differ diff --git a/doc/doxygen/extra-pages/developer_documentation/network/protocol/protocol_game_command.md b/doc/doxygen/extra-pages/developer_documentation/network/protocol/protocol_game_command.md index 18e31064a..699977fdb 100644 --- a/doc/doxygen/extra-pages/developer_documentation/network/protocol/protocol_game_command.md +++ b/doc/doxygen/extra-pages/developer_documentation/network/protocol/protocol_game_command.md @@ -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. diff --git a/doc/doxygen/groups/doc_groups.dox b/doc/doxygen/groups/doc_groups.dox index ec5158c69..d69cdc52a 100644 --- a/doc/doxygen/groups/doc_groups.dox +++ b/doc/doxygen/groups/doc_groups.dox @@ -476,8 +476,7 @@ * @ingroup GameMenus * @brief Menus for interacting with zones. * - * Provides contextual options for a CardZone such as the hand, - * library, graveyard, and battlefield. + * Provides contextual options for card zones. */ /** @@ -512,8 +511,7 @@ * @ingroup GameGraphics * @brief Graphical representations of zones. * - * Provides layout, visuals, and animations for a CardZone like the hand, - * library, battlefield, and graveyard. + * Provides layout, visuals, and animations for card zones. */ /** @@ -548,8 +546,7 @@ * @ingroup GameLogic * @brief Logical handling of CardZones during a Game. * - * Defines the rules and behaviors of zones such as the hand, - * battlefield, library, and graveyard. + * Defines the rules and behaviors of card zones. */ /** diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h index 2b1ddf8ec..c99295b2b 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h @@ -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 diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index 5c0fdf944..f5fe4c5c2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -13,16 +13,19 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -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; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index a24fa5799..c3ad8d067 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -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); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp index b18e11c2b..01a3a910f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -2,8 +2,15 @@ #include -Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) - : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) +Server_Counter::Server_Counter(int _id, + const QString &_name, + const color &_counterColor, + int _radius, + int _count, + int _minValue, + int _maxValue) + : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count), minValue(_minValue), + maxValue(_maxValue) { } @@ -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); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index ca093b7cf..9b79cae6a 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -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::max(); public: - Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count = 0); + /** + * @brief Constructs a counter. + * @param _id Unique counter identifier + * @param _name Display name + * @param _counterColor Counter color + * @param _radius Display radius + * @param _count Initial value (default 0) + * @param _minValue Minimum allowed value (default INT_MIN) + * @param _maxValue Maximum allowed value (default INT_MAX) + */ + Server_Counter(int _id, + const QString &_name, + const color &_counterColor, + int _radius, + int _count = 0, + int _minValue = std::numeric_limits::min(), + int _maxValue = DEFAULT_MAX_VALUE); ~Server_Counter() { } @@ -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::min(), std::numeric_limits::max()); + count = addClamped(count, delta, minValue, maxValue); return count != oldCount; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 4761199e5..4c838d0bf 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -67,6 +67,7 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, bool _spectatorsSeeEverything, int _startingLifeTotal, bool _shareDecklistsOnLoad, + bool _enableCommandZone, Server_Room *_room) : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)), gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers), @@ -74,9 +75,9 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed), spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk), spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), - shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), - firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), - gameMutex() + shareDecklistsOnLoad(_shareDecklistsOnLoad), enableCommandZone(_enableCommandZone), inactivityCounter(0), + startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false), + startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex() { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); @@ -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()); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index e0e7896b7..848d22b7a 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -69,6 +69,7 @@ private: bool spectatorsSeeEverything; int startingLifeTotal; bool shareDecklistsOnLoad; + bool enableCommandZone; int inactivityCounter; int startTimeOfThisGame, secondsElapsed; bool firstGameStarted; @@ -106,6 +107,7 @@ public: bool _spectatorsSeeEverything, int _startingLifeTotal, bool _shareDecklistsOnLoad, + bool _enableCommandZone, Server_Room *parent); ~Server_Game() override; Server_Room *getRoom() const @@ -173,6 +175,10 @@ public: { return shareDecklistsOnLoad; } + bool getEnableCommandZone() const + { + return enableCommandZone; + } Response::ResponseCode checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge); bool containsUser(const QString &userName) const; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index d502fc7d6..5e88f3670 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -18,14 +18,18 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include @@ -33,20 +37,26 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include +#include #include #include @@ -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()); + } } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h index 5925ed3c2..921eaf555 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h @@ -8,6 +8,7 @@ class Server_Player : public Server_AbstractPlayer Q_OBJECT private: QMap counters; + QMap castCounts; // index (1-5) -> value QList lastDrawList; public: @@ -21,9 +22,41 @@ public: { return counters; } + const QMap &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, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 5b893799f..a6543233d 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -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); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 20a4cb08d..364b575f6 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -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 diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_cast_count.proto new file mode 100644 index 000000000..15173db9a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_cast_count.proto new file mode 100644 index 000000000..96982a1d3 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_cast_count.proto new file mode 100644 index 000000000..e3fa723f8 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_cast_count.proto new file mode 100644 index 000000000..34c656449 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_cast_count.proto new file mode 100644 index 000000000..1d92becbc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_cast_count.proto new file mode 100644 index 000000000..da4a9938e --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_cast_count.proto new file mode 100644 index 000000000..c9a2b2adc --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_cast_count.proto @@ -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]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index 8ecf7ec9c..0bd9b85c0 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -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; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto index 7d3147701..fdd8143c4 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto @@ -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; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto index a8c90ec6c..0ba36fe85 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -69,6 +69,9 @@ message Command_CreateGame { // share decklists with all players when selected optional bool share_decklists_on_load = 14; + + // enable command zone for Commander format + optional bool enable_command_zone = 15; } message Command_JoinGame { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cast_count.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cast_count.proto new file mode 100644 index 000000000..7298dd43a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cast_count.proto @@ -0,0 +1,6 @@ +syntax = "proto2"; + +message ServerInfo_CastCount { + optional sint32 index = 1; + optional sint32 value = 2; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto index 849e3b4e9..af56efe13 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto @@ -7,4 +7,5 @@ message ServerInfo_Counter { optional color counter_color = 3; optional sint32 radius = 4; optional sint32 count = 5; + optional bool active = 6 [default = true]; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto index 9a56e034c..6edd2372d 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -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; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto index 69cd4498a..7309b4fcf 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto @@ -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; } diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp index ad972b433..44b0e8e87 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -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"); diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h index 24f582007..cdcea6dca 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h @@ -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); diff --git a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp index dfd67c98a..cb99d5ec7 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp @@ -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"); +} diff --git a/libcockatrice_settings/libcockatrice/settings/game_settings.h b/libcockatrice_settings/libcockatrice/settings/game_settings.h index e0a484bac..fbc814a34 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_settings.h @@ -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); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 79f5a11e4..acdc11387 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -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 diff --git a/libcockatrice_utility/libcockatrice/utility/counter_ids.h b/libcockatrice_utility/libcockatrice/utility/counter_ids.h new file mode 100644 index 000000000..46d5e9027 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/counter_ids.h @@ -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 + +/** + * @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 diff --git a/libcockatrice_utility/libcockatrice/utility/counter_limits.h b/libcockatrice_utility/libcockatrice/utility/counter_limits.h index 1343dbb3f..8a34a2948 100644 --- a/libcockatrice_utility/libcockatrice/utility/counter_limits.h +++ b/libcockatrice_utility/libcockatrice/utility/counter_limits.h @@ -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. diff --git a/libcockatrice_utility/libcockatrice/utility/zone_names.h b/libcockatrice_utility/libcockatrice/utility/zone_names.h index d1463de6a..0388cdf71 100644 --- a/libcockatrice_utility/libcockatrice/utility/zone_names.h +++ b/libcockatrice_utility/libcockatrice/utility/zone_names.h @@ -14,6 +14,9 @@ constexpr const char *DECK = "deck"; constexpr const char *SIDEBOARD = "sb"; constexpr const char *STACK = "stack"; +// Command zone (Commander format) +constexpr const char *COMMAND = "command"; + } // namespace ZoneNames #endif // ZONE_NAMES_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1c3f4c2c6..4238745bc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/command_zone_tests/CMakeLists.txt b/tests/command_zone_tests/CMakeLists.txt new file mode 100644 index 000000000..e428eae29 --- /dev/null +++ b/tests/command_zone_tests/CMakeLists.txt @@ -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) diff --git a/tests/command_zone_tests/counter_command_auth_test.cpp b/tests/command_zone_tests/counter_command_auth_test.cpp new file mode 100644 index 000000000..63cbbc590 --- /dev/null +++ b/tests/command_zone_tests/counter_command_auth_test.cpp @@ -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 +#include +#include +#include +#include + +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(); +} diff --git a/tests/command_zone_tests/new_counter_id_test.cpp b/tests/command_zone_tests/new_counter_id_test.cpp new file mode 100644 index 000000000..a3245ea5d --- /dev/null +++ b/tests/command_zone_tests/new_counter_id_test.cpp @@ -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 +#include +#include +#include +#include + +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(), 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(); +} diff --git a/tests/command_zone_tests/setup_zones_command_zone_test.cpp b/tests/command_zone_tests/setup_zones_command_zone_test.cpp new file mode 100644 index 000000000..ad4ba5cd6 --- /dev/null +++ b/tests/command_zone_tests/setup_zones_command_zone_test.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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(), + 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 &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 &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(); +} diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp index 2231a7e3b..aa0de591f 100644 --- a/tests/movecard_tests/reverse_card_move_test.cpp +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -22,7 +22,8 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest) // instantiate a fake server instance FakeServer server; Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); + Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, false, + &room); Server_AbstractPlayer player(&game, 1, user, false, nullptr); Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); diff --git a/tests/server_counter_test.cpp b/tests/server_counter_test.cpp index 0f41f2cbd..691281927 100644 --- a/tests/server_counter_test.cpp +++ b/tests/server_counter_test.cpp @@ -5,6 +5,7 @@ #include #include +#include #include TEST(ServerCounter, IncrementDoesNotOverflow) @@ -79,6 +80,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); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index dfdad4780..4762daa58 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -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)