mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-07 05:53:59 -07:00
feat(command-zone): integrate into player system
Wire command zones into the player object hierarchy, making them functional game elements. Player class modifications: - player.h/cpp: command zone ownership and lifecycle - player_actions.h/cpp: command zone action handlers - player_graphics_item.h/cpp: command zone visual positioning This commit connects all previous command zone components to the player, enabling: - Zone creation during game setup - Zone positioning relative to other player zones - Action routing for command zone interactions - Proper cleanup on player destruction
This commit is contained in:
parent
1ae1d876b1
commit
437d2e4ed4
6 changed files with 655 additions and 44 deletions
|
|
@ -5,12 +5,16 @@
|
|||
#include "../board/arrow_item.h"
|
||||
#include "../board/card_item.h"
|
||||
#include "../board/card_list.h"
|
||||
#include "../board/commander_tax_counter.h"
|
||||
#include "../board/counter_general.h"
|
||||
#include "../game_scene.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"
|
||||
#include "../zones/table_zone.h"
|
||||
#include "../zones/zone_names.h"
|
||||
#include "player_actions.h"
|
||||
#include "player_target.h"
|
||||
|
||||
|
|
@ -19,6 +23,7 @@
|
|||
#include <QMetaType>
|
||||
#include <QPainter>
|
||||
#include <QtConcurrent>
|
||||
#include <libcockatrice/common/counter_ids.h>
|
||||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_create_arrow.pb.h>
|
||||
|
|
@ -32,7 +37,8 @@
|
|||
Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent)
|
||||
: QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)),
|
||||
playerEventHandler(new PlayerEventHandler(this)), playerActions(new PlayerActions(this)), active(false),
|
||||
conceded(false), zoneId(0), dialogSemaphore(false)
|
||||
conceded(false), zoneId(0), dialogSemaphore(false), serverHasCommandZone(false), serverHasCompanionZone(false),
|
||||
serverHasBackgroundZone(false)
|
||||
{
|
||||
initializeZones();
|
||||
|
||||
|
|
@ -41,6 +47,9 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, A
|
|||
playerMenu->setMenusForGraphicItems();
|
||||
|
||||
connect(this, &Player::activeChanged, graphicsItem, &PlayerGraphicsItem::onPlayerActiveChanged);
|
||||
connect(this, &Player::commandZoneSupportChanged, graphicsItem, &PlayerGraphicsItem::setCommandZonesVisible);
|
||||
connect(this, &Player::companionZoneSupportChanged, graphicsItem, &PlayerGraphicsItem::setCompanionZoneVisible);
|
||||
connect(this, &Player::backgroundZoneSupportChanged, graphicsItem, &PlayerGraphicsItem::setBackgroundZoneVisible);
|
||||
|
||||
connect(this, &Player::openDeckEditor, game->getTab(), &TabGame::openDeckEditor);
|
||||
|
||||
|
|
@ -61,15 +70,19 @@ void Player::forwardActionSignalsToEventHandler()
|
|||
|
||||
void Player::initializeZones()
|
||||
{
|
||||
addZone(new PileZoneLogic(this, "deck", false, true, false, this));
|
||||
addZone(new PileZoneLogic(this, "grave", false, false, true, this));
|
||||
addZone(new PileZoneLogic(this, "rfg", false, false, true, this));
|
||||
addZone(new PileZoneLogic(this, "sb", false, false, false, this));
|
||||
addZone(new TableZoneLogic(this, "table", true, false, true, this));
|
||||
addZone(new StackZoneLogic(this, "stack", true, false, true, this));
|
||||
addZone(new PileZoneLogic(this, ZoneNames::DECK, false, true, false, this));
|
||||
addZone(new PileZoneLogic(this, ZoneNames::GRAVE, false, false, true, this));
|
||||
addZone(new PileZoneLogic(this, ZoneNames::EXILE, false, false, true, this));
|
||||
addZone(new PileZoneLogic(this, ZoneNames::SIDEBOARD, false, false, false, this));
|
||||
addZone(new TableZoneLogic(this, ZoneNames::TABLE, true, false, true, this));
|
||||
addZone(new StackZoneLogic(this, ZoneNames::STACK, true, false, true, this));
|
||||
bool visibleHand = playerInfo->getLocalOrJudge() ||
|
||||
(game->getPlayerManager()->isSpectator() && game->getGameMetaInfo()->spectatorsOmniscient());
|
||||
addZone(new HandZoneLogic(this, "hand", false, false, visibleHand, this));
|
||||
addZone(new HandZoneLogic(this, ZoneNames::HAND, false, false, visibleHand, this));
|
||||
addZone(new CommandZoneLogic(this, ZoneNames::COMMAND, true, false, true, this));
|
||||
addZone(new CommandZoneLogic(this, ZoneNames::PARTNER, true, false, true, this));
|
||||
addZone(new CommandZoneLogic(this, ZoneNames::COMPANION, true, false, true, this));
|
||||
addZone(new CommandZoneLogic(this, ZoneNames::BACKGROUND, true, false, true, this));
|
||||
}
|
||||
|
||||
Player::~Player()
|
||||
|
|
@ -97,6 +110,14 @@ void Player::clear()
|
|||
clearCounters();
|
||||
}
|
||||
|
||||
void Player::updateZoneSupport(bool *flag, void (Player::*signal)(bool), bool found)
|
||||
{
|
||||
if (*flag != found) {
|
||||
*flag = found;
|
||||
emit(this->*signal)(found);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::setConceded(bool _conceded)
|
||||
{
|
||||
if (conceded != _conceded) {
|
||||
|
|
@ -125,7 +146,9 @@ void Player::processPlayerInfo(const ServerInfo_Player &info)
|
|||
/* StackZone */
|
||||
"stack",
|
||||
/* HandZone */
|
||||
"hand"};
|
||||
"hand",
|
||||
/* CommandZones */
|
||||
"command", "partner", "companion", "background"};
|
||||
clearCounters();
|
||||
clearArrows();
|
||||
|
||||
|
|
@ -140,7 +163,27 @@ void Player::processPlayerInfo(const ServerInfo_Player &info)
|
|||
|
||||
emit clearCustomZonesMenu();
|
||||
|
||||
// Check if server has command zones by scanning the zone list
|
||||
bool foundCommandZone = false;
|
||||
bool foundCompanionZone = false;
|
||||
bool foundBackgroundZone = false;
|
||||
const int zoneListSize = info.zone_list_size();
|
||||
for (int i = 0; i < zoneListSize; ++i) {
|
||||
QString zoneName = QString::fromStdString(info.zone_list(i).name());
|
||||
if (zoneName == ZoneNames::COMMAND) {
|
||||
foundCommandZone = true;
|
||||
} else if (zoneName == ZoneNames::COMPANION) {
|
||||
foundCompanionZone = true;
|
||||
} else if (zoneName == ZoneNames::BACKGROUND) {
|
||||
foundBackgroundZone = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update command zone support flags and notify graphics
|
||||
updateZoneSupport(&serverHasCommandZone, &Player::commandZoneSupportChanged, foundCommandZone);
|
||||
updateZoneSupport(&serverHasCompanionZone, &Player::companionZoneSupportChanged, foundCompanionZone);
|
||||
updateZoneSupport(&serverHasBackgroundZone, &Player::backgroundZoneSupportChanged, foundBackgroundZone);
|
||||
|
||||
for (int i = 0; i < zoneListSize; ++i) {
|
||||
const ServerInfo_Zone &zoneInfo = info.zone_list(i);
|
||||
|
||||
|
|
@ -156,7 +199,7 @@ void Player::processPlayerInfo(const ServerInfo_Player &info)
|
|||
// Zones without coordinats are always treated as non-shufflable
|
||||
// PileZones, although supporting alternate hand or stack zones
|
||||
// might make sense in some scenarios.
|
||||
bool contentsKnown;
|
||||
bool contentsKnown = false; // Default to false for unknown zone types
|
||||
|
||||
switch (zoneInfo.type()) {
|
||||
case ServerInfo_Zone::PrivateZone:
|
||||
|
|
@ -172,6 +215,9 @@ void Player::processPlayerInfo(const ServerInfo_Player &info)
|
|||
case ServerInfo_Zone::HiddenZone:
|
||||
contentsKnown = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
zone = addZone(new PileZoneLogic(this, zoneName, false, /* isShufflable */ false, contentsKnown, this));
|
||||
|
|
@ -285,6 +331,20 @@ AbstractCounter *Player::addCounter(int counterId, const QString &name, QColor c
|
|||
AbstractCounter *ctr;
|
||||
if (name == "life") {
|
||||
ctr = getGraphicsItem()->getPlayerTarget()->addCounter(counterId, name, value);
|
||||
} else if (counterId == CounterIds::CommanderTax) {
|
||||
QGraphicsItem *parent = graphicsItem->getCommandZoneGraphicsItem();
|
||||
if (!parent) {
|
||||
qCWarning(PlayerLog) << "addCounter: Cannot create CommanderTax counter - command zone not available";
|
||||
return nullptr;
|
||||
}
|
||||
ctr = new CommanderTaxCounter(this, counterId, name, ZoneSizes::TAX_COUNTER_SIZE, value, parent);
|
||||
} else if (counterId == CounterIds::PartnerTax) {
|
||||
QGraphicsItem *parent = graphicsItem->getPartnerZoneGraphicsItem();
|
||||
if (!parent) {
|
||||
qCWarning(PlayerLog) << "addCounter: Cannot create PartnerTax counter - partner zone not available";
|
||||
return nullptr;
|
||||
}
|
||||
ctr = new CommanderTaxCounter(this, counterId, name, ZoneSizes::TAX_COUNTER_SIZE, value, parent);
|
||||
} else {
|
||||
ctr = new GeneralCounter(this, counterId, name, color, radius, value, true, graphicsItem);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "../../game_graphics/board/abstract_graphics_item.h"
|
||||
#include "../../interface/widgets/menus/tearoff_menu.h"
|
||||
#include "../interface/deck_loader/loaded_deck.h"
|
||||
#include "../zones/logic/command_zone_logic.h"
|
||||
#include "../zones/logic/hand_zone_logic.h"
|
||||
#include "../zones/logic/pile_zone_logic.h"
|
||||
#include "../zones/logic/stack_zone_logic.h"
|
||||
|
|
@ -75,6 +76,9 @@ signals:
|
|||
void clearCustomZonesMenu();
|
||||
void addViewCustomZoneActionToCustomZoneMenu(QString zoneName);
|
||||
void resetTopCardMenuActions();
|
||||
void commandZoneSupportChanged(bool hasCommandZone);
|
||||
void companionZoneSupportChanged(bool hasCompanionZone);
|
||||
void backgroundZoneSupportChanged(bool hasBackgroundZone);
|
||||
|
||||
public slots:
|
||||
void setActive(bool _active);
|
||||
|
|
@ -143,7 +147,7 @@ public:
|
|||
return zone;
|
||||
}
|
||||
|
||||
CardZoneLogic *getZone(const QString zoneName)
|
||||
CardZoneLogic *getZone(const QString &zoneName)
|
||||
{
|
||||
return zones.value(zoneName);
|
||||
}
|
||||
|
|
@ -188,6 +192,26 @@ public:
|
|||
return qobject_cast<HandZoneLogic *>(zones.value("hand"));
|
||||
}
|
||||
|
||||
CommandZoneLogic *getCommandZone()
|
||||
{
|
||||
return qobject_cast<CommandZoneLogic *>(zones.value("command"));
|
||||
}
|
||||
|
||||
CommandZoneLogic *getPartnerZone()
|
||||
{
|
||||
return qobject_cast<CommandZoneLogic *>(zones.value("partner"));
|
||||
}
|
||||
|
||||
CommandZoneLogic *getCompanionZone()
|
||||
{
|
||||
return qobject_cast<CommandZoneLogic *>(zones.value("companion"));
|
||||
}
|
||||
|
||||
CommandZoneLogic *getBackgroundZone()
|
||||
{
|
||||
return qobject_cast<CommandZoneLogic *>(zones.value("background"));
|
||||
}
|
||||
|
||||
AbstractCounter *addCounter(const ServerInfo_Counter &counter);
|
||||
AbstractCounter *addCounter(int counterId, const QString &name, QColor color, int radius, int value);
|
||||
void delCounter(int counterId);
|
||||
|
|
@ -216,6 +240,21 @@ public:
|
|||
return conceded;
|
||||
}
|
||||
|
||||
bool hasServerCommandZone() const
|
||||
{
|
||||
return serverHasCommandZone;
|
||||
}
|
||||
|
||||
bool hasServerCompanionZone() const
|
||||
{
|
||||
return serverHasCompanionZone;
|
||||
}
|
||||
|
||||
bool hasServerBackgroundZone() const
|
||||
{
|
||||
return serverHasBackgroundZone;
|
||||
}
|
||||
|
||||
void setGameStarted();
|
||||
|
||||
void setDialogSemaphore(const bool _active)
|
||||
|
|
@ -249,8 +288,13 @@ private:
|
|||
QMap<int, ArrowItem *> arrows;
|
||||
|
||||
bool dialogSemaphore;
|
||||
bool serverHasCommandZone;
|
||||
bool serverHasCompanionZone;
|
||||
bool serverHasBackgroundZone;
|
||||
QList<CardItem *> cardsToDelete;
|
||||
|
||||
void updateZoneSupport(bool *flag, void (Player::*signal)(bool), bool found);
|
||||
|
||||
// void eventConnectionStateChanged(const Event_ConnectionStateChanged &event);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
#include "../zones/hand_zone.h"
|
||||
#include "../zones/logic/view_zone_logic.h"
|
||||
#include "../zones/table_zone.h"
|
||||
#include "../zones/zone_names.h"
|
||||
#include "card_menu_action_type.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
#include <libcockatrice/common/counter_ids.h>
|
||||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_create_token.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_flip_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_game_say.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_mulligan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_reveal_cards.pb.h>
|
||||
|
|
@ -32,6 +37,15 @@
|
|||
// milliseconds in between triggers of the move top cards until action
|
||||
static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100;
|
||||
|
||||
// Lookup table for move-to-zone actions -> zone name strings
|
||||
// Consolidates 4 identical switch cases into data-driven dispatch
|
||||
static const QHash<CardMenuActionType, QString> moveToZoneMap = {
|
||||
{cmMoveToCommandZone, ZoneNames::COMMAND},
|
||||
{cmMoveToPartnerZone, ZoneNames::PARTNER},
|
||||
{cmMoveToCompanionZone, ZoneNames::COMPANION},
|
||||
{cmMoveToBackgroundZone, ZoneNames::BACKGROUND},
|
||||
};
|
||||
|
||||
PlayerActions::PlayerActions(Player *_player) : player(_player), lastTokenTableRow(0), movingCardsUntil(false)
|
||||
{
|
||||
moveTopCardTimer = new QTimer(this);
|
||||
|
|
@ -520,7 +534,13 @@ void PlayerActions::moveOneCardUntil(CardItem *card)
|
|||
if (isMatch && movingCardsUntilAutoPlay) {
|
||||
// Directly calling playCard will deadlock, since we are already in the middle of processing an event.
|
||||
// Use QTimer::singleShot to queue up the playCard on the event loop.
|
||||
QTimer::singleShot(0, this, [card, this] { playCard(card, false); });
|
||||
// Use QPointer to safely handle the case where the card is deleted before the timer fires.
|
||||
QPointer<CardItem> safeCard = card;
|
||||
QTimer::singleShot(0, this, [safeCard, this] {
|
||||
if (safeCard) {
|
||||
playCard(safeCard, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (player->getDeckZone()->getCards().empty() || !card) {
|
||||
|
|
@ -1280,7 +1300,10 @@ void PlayerActions::actIncPT(int deltaP, int deltaT)
|
|||
|
||||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
QString pt = card->getPT();
|
||||
const auto ptList = parsePT(pt);
|
||||
QString newpt;
|
||||
|
|
@ -1313,7 +1336,10 @@ void PlayerActions::actResetPT()
|
|||
int playerid = player->getPlayerInfo()->getId();
|
||||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
QString ptString;
|
||||
if (!card->getFaceDown()) { // leave the pt empty if the card is face down
|
||||
ExactCard ec = card->getCard();
|
||||
|
|
@ -1380,8 +1406,8 @@ void PlayerActions::actSetPT()
|
|||
|
||||
auto sel = player->getGameScene()->selectedItems();
|
||||
for (const auto &item : sel) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
if (!card->getPT().isEmpty()) {
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (card && !card->getPT().isEmpty()) {
|
||||
oldPT = card->getPT();
|
||||
}
|
||||
}
|
||||
|
|
@ -1399,7 +1425,10 @@ void PlayerActions::actSetPT()
|
|||
|
||||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (const auto &item : sel) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
auto *cmd = new Command_SetCardAttr;
|
||||
QString newpt = QString();
|
||||
if (!empty) {
|
||||
|
|
@ -1498,8 +1527,8 @@ void PlayerActions::actSetAnnotation()
|
|||
QString oldAnnotation;
|
||||
auto sel = player->getGameScene()->selectedItems();
|
||||
for (const auto &item : sel) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
if (!card->getAnnotation().isEmpty()) {
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (card && !card->getAnnotation().isEmpty()) {
|
||||
oldAnnotation = card->getAnnotation();
|
||||
}
|
||||
}
|
||||
|
|
@ -1519,7 +1548,10 @@ void PlayerActions::actSetAnnotation()
|
|||
|
||||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (const auto &item : sel) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
auto *cmd = new Command_SetCardAttr;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
cmd->set_card_id(card->getId());
|
||||
|
|
@ -1544,9 +1576,8 @@ void PlayerActions::actUnattach()
|
|||
{
|
||||
QList<const ::google::protobuf::Message *> commandList;
|
||||
for (QGraphicsItem *item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
|
||||
if (!card->getAttachedTo()) {
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (!card || !card->getAttachedTo()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1566,8 +1597,8 @@ void PlayerActions::actCardCounterTrigger()
|
|||
switch (action->data().toInt() % 1000) {
|
||||
case 9: { // increment counter
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
if (card->getCounters().value(counterId, 0) < MAX_COUNTERS_ON_CARD) {
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (card && card->getCounters().value(counterId, 0) < MAX_COUNTERS_ON_CARD) {
|
||||
auto *cmd = new Command_SetCardCounter;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
cmd->set_card_id(card->getId());
|
||||
|
|
@ -1580,8 +1611,8 @@ void PlayerActions::actCardCounterTrigger()
|
|||
}
|
||||
case 10: { // decrement counter
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
if (card->getCounters().value(counterId, 0)) {
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (card && card->getCounters().value(counterId, 0)) {
|
||||
auto *cmd = new Command_SetCardCounter;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
cmd->set_card_id(card->getId());
|
||||
|
|
@ -1598,8 +1629,9 @@ void PlayerActions::actCardCounterTrigger()
|
|||
|
||||
int oldValue = 0;
|
||||
if (player->getGameScene()->selectedItems().size() == 1) {
|
||||
auto *card = static_cast<CardItem *>(player->getGameScene()->selectedItems().first());
|
||||
oldValue = card->getCounters().value(counterId, 0);
|
||||
if (auto *card = qgraphicsitem_cast<CardItem *>(player->getGameScene()->selectedItems().first())) {
|
||||
oldValue = card->getCounters().value(counterId, 0);
|
||||
}
|
||||
}
|
||||
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Set counters"), tr("Number:"), oldValue,
|
||||
0, MAX_COUNTERS_ON_CARD, 1, &ok);
|
||||
|
|
@ -1609,7 +1641,10 @@ void PlayerActions::actCardCounterTrigger()
|
|||
}
|
||||
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
auto *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
auto *cmd = new Command_SetCardCounter;
|
||||
cmd->set_zone(card->getZone()->getName().toStdString());
|
||||
cmd->set_card_id(card->getId());
|
||||
|
|
@ -1640,8 +1675,9 @@ void PlayerActions::playSelectedCards(const bool faceDown)
|
|||
{
|
||||
QList<CardItem *> selectedCards;
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
selectedCards.append(card);
|
||||
if (auto *card = qgraphicsitem_cast<CardItem *>(item)) {
|
||||
selectedCards.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
// CardIds will get shuffled downwards when cards leave the deck.
|
||||
|
|
@ -1667,12 +1703,52 @@ void PlayerActions::actPlayFacedown()
|
|||
playSelectedCards(true);
|
||||
}
|
||||
|
||||
void PlayerActions::actPlayAndIncreaseTax()
|
||||
{
|
||||
QList<CardItem *> selectedCards;
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
if (auto *card = qgraphicsitem_cast<CardItem *>(item)) {
|
||||
selectedCards.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(selectedCards.begin(), selectedCards.end(),
|
||||
[](const auto &card1, const auto &card2) { return card1->getId() > card2->getId(); });
|
||||
|
||||
for (auto &card : selectedCards) {
|
||||
if (!card || isUnwritableRevealZone(card->getZone()) || card->getZone()->getName() == ZoneNames::TABLE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString zoneName = card->getZone()->getName();
|
||||
|
||||
// Only increment tax for cards from command or partner zones
|
||||
if (zoneName == ZoneNames::COMMAND || zoneName == ZoneNames::PARTNER) {
|
||||
// Determine the correct counter ID
|
||||
int counterId = (zoneName == ZoneNames::PARTNER) ? CounterIds::PartnerTax : CounterIds::CommanderTax;
|
||||
|
||||
// Play the card
|
||||
playCard(card, false);
|
||||
|
||||
// Increment the tax counter
|
||||
Command_IncCounter cmd;
|
||||
cmd.set_counter_id(counterId);
|
||||
cmd.set_delta(1);
|
||||
sendGameCommand(cmd);
|
||||
} else {
|
||||
// For non-command zone cards, just play normally
|
||||
playCard(card, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerActions::actHide()
|
||||
{
|
||||
for (const auto &item : player->getGameScene()->selectedItems()) {
|
||||
auto *card = static_cast<CardItem *>(item);
|
||||
if (card && isUnwritableRevealZone(card->getZone())) {
|
||||
card->getZone()->removeCard(card);
|
||||
if (auto *card = qgraphicsitem_cast<CardItem *>(item)) {
|
||||
if (isUnwritableRevealZone(card->getZone())) {
|
||||
card->getZone()->removeCard(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1776,7 +1852,14 @@ void PlayerActions::cardMenuAction()
|
|||
QList<QGraphicsItem *> sel = player->getGameScene()->selectedItems();
|
||||
QList<CardItem *> cardList;
|
||||
while (!sel.isEmpty()) {
|
||||
cardList.append(qgraphicsitem_cast<CardItem *>(sel.takeFirst()));
|
||||
CardItem *card = qgraphicsitem_cast<CardItem *>(sel.takeFirst());
|
||||
if (card) {
|
||||
cardList.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
if (cardList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QList<const ::google::protobuf::Message *> commandList;
|
||||
|
|
@ -1863,7 +1946,8 @@ void PlayerActions::cardMenuAction()
|
|||
idList.add_card()->set_card_id(i->getId());
|
||||
}
|
||||
|
||||
switch (static_cast<CardMenuActionType>(a->data().toInt())) {
|
||||
const auto actionType = static_cast<CardMenuActionType>(a->data().toInt());
|
||||
switch (actionType) {
|
||||
case cmMoveToTopLibrary: {
|
||||
auto *cmd = new Command_MoveCard;
|
||||
cmd->set_start_player_id(startPlayerId);
|
||||
|
|
@ -1944,6 +2028,58 @@ void PlayerActions::cardMenuAction()
|
|||
commandList.append(cmd);
|
||||
break;
|
||||
}
|
||||
case cmMoveToCommandZone:
|
||||
case cmMoveToPartnerZone:
|
||||
case cmMoveToCompanionZone:
|
||||
case cmMoveToBackgroundZone: {
|
||||
const QString zoneName = moveToZoneMap.value(actionType);
|
||||
if (zoneName.isEmpty()) {
|
||||
qCWarning(PlayerLog) << "Unknown move-to-zone action:" << static_cast<int>(actionType);
|
||||
break;
|
||||
}
|
||||
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(zoneName.toStdString());
|
||||
cmd->set_x(0);
|
||||
cmd->set_y(0);
|
||||
commandList.append(cmd);
|
||||
break;
|
||||
}
|
||||
case cmMoveToTable: {
|
||||
// Process each card individually to position based on card type
|
||||
for (const auto &card : cardList) {
|
||||
auto *cmd = new Command_MoveCard;
|
||||
cmd->set_start_player_id(startPlayerId);
|
||||
cmd->set_start_zone(startZone.toStdString());
|
||||
cmd->set_target_player_id(player->getPlayerInfo()->getId());
|
||||
cmd->set_target_zone("table");
|
||||
|
||||
CardToMove *cardToMove = cmd->mutable_cards_to_move()->add_card();
|
||||
cardToMove->set_card_id(card->getId());
|
||||
|
||||
// Determine correct row based on card type (same logic as playCardToTable)
|
||||
int tableRow = 1; // default for non-creatures
|
||||
ExactCard exactCard = card->getCard();
|
||||
if (exactCard) {
|
||||
const CardInfo &info = exactCard.getInfo();
|
||||
tableRow = info.getUiAttributes().tableRow;
|
||||
// default instant/sorcery cards to the noncreatures row
|
||||
if (tableRow > 2) {
|
||||
tableRow = 1;
|
||||
}
|
||||
cardToMove->set_pt(info.getPowTough().toStdString());
|
||||
cardToMove->set_tapped(info.getUiAttributes().cipt);
|
||||
}
|
||||
|
||||
cmd->set_x(-1);
|
||||
cmd->set_y(TableZone::clampValidTableRow(2 - tableRow));
|
||||
commandList.append(cmd);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ public slots:
|
|||
|
||||
void actPlay();
|
||||
void actPlayFacedown();
|
||||
void actPlayAndIncreaseTax();
|
||||
void actHide();
|
||||
|
||||
void actMoveTopCardToPlay();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
#include "player_graphics_item.h"
|
||||
|
||||
// @see tests/command_zone/counter_visibility_test.cpp - test harness duplicates visibility logic
|
||||
// @see tests/command_zone/command_zone_integration_test.cpp - test harness duplicates counter positioning
|
||||
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
#include "../board/commander_tax_counter.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"
|
||||
#include "../zones/table_zone.h"
|
||||
|
||||
#include <libcockatrice/common/counter_ids.h>
|
||||
|
||||
PlayerGraphicsItem::PlayerGraphicsItem(Player *_player) : player(_player)
|
||||
{
|
||||
connect(&SettingsCache::instance(), &SettingsCache::horizontalHandChanged, this,
|
||||
|
|
@ -83,6 +91,54 @@ void PlayerGraphicsItem::initializeZones()
|
|||
handZoneGraphicsItem =
|
||||
new HandZone(player->getHandZone(), static_cast<int>(tableZoneGraphicsItem->boundingRect().height()), this);
|
||||
|
||||
// Command zones - mixed hierarchy:
|
||||
// - Partner is Qt child of Command (tight coupling)
|
||||
// - Companion and Background are Qt siblings (direct children of PlayerGraphicsItem)
|
||||
// with logical parent-child relationships for hierarchy traversal
|
||||
//
|
||||
// OWNERSHIP INVARIANT: All command zones must remain Qt children of either
|
||||
// CommandZone (for Partner) or PlayerGraphicsItem (for Primary, Companion, Background)
|
||||
// for the duration of their lifetime. The logical parent-child relationships
|
||||
// (logicalParentZone, logicalChildZones) are raw pointers that rely on this
|
||||
// ownership guarantee. Do not reparent zones or change destruction order.
|
||||
|
||||
// Primary command zone
|
||||
commandZoneGraphicsItem =
|
||||
new CommandZone(player->getCommandZone(), ZoneSizes::COMMAND_ZONE_HEIGHT, CommandZoneType::Primary, this);
|
||||
commandZoneGraphicsItem->setZValue(ZValues::COMMAND_ZONE);
|
||||
commandZoneGraphicsItem->setVisible(false);
|
||||
connect(commandZoneGraphicsItem, &CommandZone::minimizedChanged, this, &PlayerGraphicsItem::rearrangeZones);
|
||||
|
||||
// Partner zone is Qt child of Command (nested - preserves tight Command-Partner relationship)
|
||||
partnerZoneGraphicsItem = new CommandZone(player->getPartnerZone(), ZoneSizes::COMMAND_ZONE_HEIGHT,
|
||||
CommandZoneType::Partner, commandZoneGraphicsItem);
|
||||
partnerZoneGraphicsItem->setZValue(ZValues::PARTNER_ZONE);
|
||||
connect(partnerZoneGraphicsItem, &CommandZone::expandedChanged, this, &PlayerGraphicsItem::onZoneExpanded);
|
||||
connect(partnerZoneGraphicsItem, &CommandZone::minimizedChanged, this, &PlayerGraphicsItem::rearrangeZones);
|
||||
|
||||
// Companion zone is SIBLING - direct child of PlayerGraphicsItem (fixes spacing bugs)
|
||||
companionZoneGraphicsItem =
|
||||
new CommandZone(player->getCompanionZone(), ZoneSizes::COMMAND_ZONE_HEIGHT, CommandZoneType::Companion, this);
|
||||
companionZoneGraphicsItem->setZValue(ZValues::PARTNER_ZONE + 1);
|
||||
companionZoneGraphicsItem->setVisible(false);
|
||||
connect(companionZoneGraphicsItem, &CommandZone::expandedChanged, this, &PlayerGraphicsItem::onZoneExpanded);
|
||||
connect(companionZoneGraphicsItem, &CommandZone::minimizedChanged, this, &PlayerGraphicsItem::rearrangeZones);
|
||||
|
||||
// Background zone is SIBLING - direct child of PlayerGraphicsItem (fixes spacing bugs)
|
||||
backgroundZoneGraphicsItem =
|
||||
new CommandZone(player->getBackgroundZone(), ZoneSizes::COMMAND_ZONE_HEIGHT, CommandZoneType::Background, this);
|
||||
backgroundZoneGraphicsItem->setZValue(ZValues::PARTNER_ZONE + 2);
|
||||
backgroundZoneGraphicsItem->setVisible(false);
|
||||
connect(backgroundZoneGraphicsItem, &CommandZone::expandedChanged, this, &PlayerGraphicsItem::onZoneExpanded);
|
||||
connect(backgroundZoneGraphicsItem, &CommandZone::minimizedChanged, this, &PlayerGraphicsItem::rearrangeZones);
|
||||
|
||||
// Set up logical parent-child relationships for sibling zones
|
||||
// (used by hierarchy traversal methods like tryMinimizeAbove, shouldPreventCollapse)
|
||||
companionZoneGraphicsItem->setLogicalParentZone(partnerZoneGraphicsItem);
|
||||
backgroundZoneGraphicsItem->setLogicalParentZone(companionZoneGraphicsItem);
|
||||
partnerZoneGraphicsItem->addLogicalChildZone(companionZoneGraphicsItem);
|
||||
companionZoneGraphicsItem->addLogicalChildZone(backgroundZoneGraphicsItem);
|
||||
|
||||
connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter,
|
||||
&HandCounter::updateNumber);
|
||||
connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu);
|
||||
|
|
@ -111,7 +167,6 @@ void PlayerGraphicsItem::paint(QPainter * /*painter*/,
|
|||
|
||||
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
||||
{
|
||||
// Extend table (and hand, if horizontal) to accommodate the new player width.
|
||||
qreal tableWidth =
|
||||
newPlayerWidth - CARD_HEIGHT - 15 - counterAreaWidth - stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().getHorizontalHand()) {
|
||||
|
|
@ -136,10 +191,33 @@ void PlayerGraphicsItem::rearrangeCounters()
|
|||
const qreal padding = 5;
|
||||
qreal ySize = boundingRect().y() + marginTop;
|
||||
|
||||
// Place objects
|
||||
bool commandZonesVisible = commandZoneGraphicsItem->isVisible();
|
||||
|
||||
for (const auto &counter : player->getCounters()) {
|
||||
AbstractCounter *ctr = counter;
|
||||
|
||||
if (ctr->getId() == CounterIds::CommanderTax) {
|
||||
if (commandZonesVisible) {
|
||||
ctr->setPos(2, 2);
|
||||
ctr->setZValue(ZValues::TAX_COUNTERS);
|
||||
ctr->setVisible(true);
|
||||
} else {
|
||||
ctr->setVisible(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ctr->getId() == CounterIds::PartnerTax) {
|
||||
if (commandZonesVisible && partnerZoneGraphicsItem->isExpanded()) {
|
||||
ctr->setPos(2, 2);
|
||||
ctr->setZValue(ZValues::TAX_COUNTERS);
|
||||
ctr->setVisible(true);
|
||||
} else {
|
||||
ctr->setVisible(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ctr->getShownInCounterArea()) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -150,9 +228,132 @@ void PlayerGraphicsItem::rearrangeCounters()
|
|||
}
|
||||
}
|
||||
|
||||
qreal PlayerGraphicsItem::positionCommandZones(const QPointF &base, bool commandZonesVisible)
|
||||
{
|
||||
qreal runningY = 0;
|
||||
|
||||
// Position command/partner zones only if command zone is visible
|
||||
if (commandZonesVisible) {
|
||||
commandZoneGraphicsItem->setPos(base);
|
||||
partnerZoneGraphicsItem->setPos(0, commandZoneGraphicsItem->currentHeight());
|
||||
runningY = commandZoneGraphicsItem->currentHeight() + partnerZoneGraphicsItem->currentHeight();
|
||||
}
|
||||
|
||||
// Companion and background are Qt siblings - position based on their own visibility
|
||||
bool companionZoneEnabled = companionZoneGraphicsItem->isVisible();
|
||||
bool backgroundZoneEnabled = backgroundZoneGraphicsItem->isVisible();
|
||||
|
||||
if (companionZoneEnabled) {
|
||||
companionZoneGraphicsItem->setPos(base.x(), base.y() + runningY);
|
||||
runningY += companionZoneGraphicsItem->currentHeight();
|
||||
}
|
||||
|
||||
if (backgroundZoneEnabled) {
|
||||
backgroundZoneGraphicsItem->setPos(base.x(), base.y() + runningY);
|
||||
runningY += backgroundZoneGraphicsItem->currentHeight();
|
||||
}
|
||||
|
||||
// Toggle visibility rules with availability guards:
|
||||
// A zone's toggle is ONLY visible if:
|
||||
// 1. The zone itself is enabled (Qt visible), AND
|
||||
// 2. Either the zone is open OR it's the "next available" to open
|
||||
//
|
||||
// Truth table for entry point (game start, nothing open):
|
||||
// | Cmd Enabled | Comp Enabled | Bg Enabled | Entry Point Toggle |
|
||||
// |-------------|--------------|------------|--------------------|
|
||||
// | Y | Y | Y | Partner |
|
||||
// | Y | N | Y | Partner |
|
||||
// | N | Y | Y | Companion |
|
||||
// | N | Y | N | Companion |
|
||||
// | N | N | Y | Background |
|
||||
if (commandZonesVisible || companionZoneEnabled || backgroundZoneEnabled) {
|
||||
bool commandZoneEnabled = commandZonesVisible;
|
||||
|
||||
bool partnerCollapsed = partnerZoneGraphicsItem->isCollapsed();
|
||||
bool companionCollapsed = companionZoneGraphicsItem->isCollapsed();
|
||||
bool backgroundCollapsed = backgroundZoneGraphicsItem->isCollapsed();
|
||||
|
||||
bool partnerIsOpen = !partnerCollapsed;
|
||||
bool companionIsOpen = !companionCollapsed;
|
||||
bool backgroundIsOpen = !backgroundCollapsed;
|
||||
bool noDeeperZonesOpen = companionCollapsed && backgroundCollapsed;
|
||||
|
||||
// Partner toggle: visible when open OR at game start (no deeper zones open)
|
||||
partnerZoneGraphicsItem->setToggleButtonVisible(commandZoneEnabled && (partnerIsOpen || noDeeperZonesOpen));
|
||||
|
||||
// Companion toggle: visible when open OR when it's next in cascade OR entry point
|
||||
companionZoneGraphicsItem->setToggleButtonVisible(
|
||||
companionZoneEnabled &&
|
||||
(companionIsOpen || (partnerIsOpen && backgroundCollapsed) || (!commandZoneEnabled && noDeeperZonesOpen)));
|
||||
|
||||
// Background toggle: visible when open OR parent open OR skipping disabled companion
|
||||
backgroundZoneGraphicsItem->setToggleButtonVisible(
|
||||
backgroundZoneEnabled && (backgroundIsOpen || companionIsOpen || (partnerIsOpen && !companionZoneEnabled) ||
|
||||
(!commandZoneEnabled && !companionZoneEnabled)));
|
||||
}
|
||||
|
||||
stackZoneGraphicsItem->setPos(base + QPointF(0, runningY));
|
||||
|
||||
return runningY;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::rearrangeZones()
|
||||
{
|
||||
auto base = QPointF(CARD_HEIGHT + counterAreaWidth + 15, 0);
|
||||
|
||||
bool commandZoneEnabled = commandZoneGraphicsItem->isVisible();
|
||||
bool companionZoneEnabled = companionZoneGraphicsItem->isVisible();
|
||||
bool backgroundZoneEnabled = backgroundZoneGraphicsItem->isVisible();
|
||||
bool anyCommandFamilyZonesVisible = commandZoneEnabled || companionZoneEnabled || backgroundZoneEnabled;
|
||||
|
||||
// Calculate available stack height, accounting for command zones if present
|
||||
qreal tableHeight = tableZoneGraphicsItem->boundingRect().height();
|
||||
qreal stackHeight = tableHeight;
|
||||
if (anyCommandFamilyZonesVisible) {
|
||||
qreal totalCommandHeight = totalCommandZoneHeight();
|
||||
|
||||
// Safety-net auto-minimize for non-expansion triggers (window resize, etc.)
|
||||
// Most overflow is now handled at expansion time by onZoneExpanded()
|
||||
constexpr int MAX_MINIMIZE_ITERATIONS = 3;
|
||||
int iterations = 0;
|
||||
while (totalCommandHeight > tableHeight - ZoneSizes::MINIMUM_STACK_HEIGHT &&
|
||||
iterations < MAX_MINIMIZE_ITERATIONS) {
|
||||
bool minimized = false;
|
||||
|
||||
// Try command zone hierarchy first (includes partner via logical children)
|
||||
if (commandZoneEnabled) {
|
||||
minimized = commandZoneGraphicsItem->tryAutoMinimize();
|
||||
}
|
||||
|
||||
// Fall back to companion/background directly when command zone is disabled.
|
||||
// These zones use toggleMinimized() directly since they don't have the same
|
||||
// hierarchical auto-minimize traversal that command zone provides.
|
||||
if (!minimized && companionZoneEnabled && companionZoneGraphicsItem->isExpanded() &&
|
||||
!companionZoneGraphicsItem->isMinimized()) {
|
||||
companionZoneGraphicsItem->toggleMinimized();
|
||||
minimized = true;
|
||||
}
|
||||
|
||||
if (!minimized && backgroundZoneEnabled && backgroundZoneGraphicsItem->isExpanded() &&
|
||||
!backgroundZoneGraphicsItem->isMinimized()) {
|
||||
backgroundZoneGraphicsItem->toggleMinimized();
|
||||
minimized = true;
|
||||
}
|
||||
|
||||
if (!minimized)
|
||||
break;
|
||||
|
||||
totalCommandHeight = totalCommandZoneHeight();
|
||||
++iterations;
|
||||
}
|
||||
|
||||
stackHeight = tableHeight - totalCommandHeight;
|
||||
if (stackHeight < ZoneSizes::MINIMUM_STACK_HEIGHT) {
|
||||
stackHeight = ZoneSizes::MINIMUM_STACK_HEIGHT;
|
||||
}
|
||||
}
|
||||
stackZoneGraphicsItem->setHeight(stackHeight);
|
||||
|
||||
if (SettingsCache::instance().getHorizontalHand()) {
|
||||
if (mirrored) {
|
||||
if (player->getHandZone()->contentsKnown()) {
|
||||
|
|
@ -163,12 +364,12 @@ void PlayerGraphicsItem::rearrangeZones()
|
|||
player->getPlayerInfo()->setHandVisible(false);
|
||||
}
|
||||
|
||||
stackZoneGraphicsItem->setPos(base);
|
||||
positionCommandZones(base, commandZoneEnabled);
|
||||
base += QPointF(stackZoneGraphicsItem->boundingRect().width(), 0);
|
||||
|
||||
tableZoneGraphicsItem->setPos(base);
|
||||
} else {
|
||||
stackZoneGraphicsItem->setPos(base);
|
||||
positionCommandZones(base, commandZoneEnabled);
|
||||
|
||||
tableZoneGraphicsItem->setPos(base.x() + stackZoneGraphicsItem->boundingRect().width(), 0);
|
||||
base += QPointF(0, tableZoneGraphicsItem->boundingRect().height());
|
||||
|
|
@ -188,7 +389,7 @@ void PlayerGraphicsItem::rearrangeZones()
|
|||
handZoneGraphicsItem->setPos(base);
|
||||
base += QPointF(handZoneGraphicsItem->boundingRect().width(), 0);
|
||||
|
||||
stackZoneGraphicsItem->setPos(base);
|
||||
positionCommandZones(base, commandZoneEnabled);
|
||||
base += QPointF(stackZoneGraphicsItem->boundingRect().width(), 0);
|
||||
|
||||
tableZoneGraphicsItem->setPos(base);
|
||||
|
|
@ -217,4 +418,93 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
playerArea->setSize(CARD_HEIGHT + counterAreaWidth + 15, bRect.height());
|
||||
|
||||
emit sizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal PlayerGraphicsItem::totalCommandZoneHeight() const
|
||||
{
|
||||
qreal total = 0;
|
||||
|
||||
// Command zone and partner use Qt parent-child - checking command visibility
|
||||
// implicitly covers partner (Qt hides children when parent is hidden)
|
||||
if (commandZoneGraphicsItem->isVisible()) {
|
||||
total += commandZoneGraphicsItem->currentHeight();
|
||||
if (partnerZoneGraphicsItem->isExpanded()) {
|
||||
total += partnerZoneGraphicsItem->currentHeight();
|
||||
}
|
||||
}
|
||||
|
||||
// Sibling zones have independent visibility - check each one
|
||||
if (companionZoneGraphicsItem->isVisible()) {
|
||||
total += companionZoneGraphicsItem->currentHeight();
|
||||
}
|
||||
|
||||
if (backgroundZoneGraphicsItem->isVisible()) {
|
||||
total += backgroundZoneGraphicsItem->currentHeight();
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::onZoneExpanded(bool expanded)
|
||||
{
|
||||
if (!expanded) {
|
||||
rearrangeZones();
|
||||
return;
|
||||
}
|
||||
|
||||
auto *expandedZone = qobject_cast<CommandZone *>(sender());
|
||||
if (expandedZone == nullptr) {
|
||||
rearrangeZones();
|
||||
return;
|
||||
}
|
||||
|
||||
qreal tableHeight = tableZoneGraphicsItem->boundingRect().height();
|
||||
qreal totalHeight = totalCommandZoneHeight();
|
||||
|
||||
constexpr int MAX_MINIMIZE_ITERATIONS = 3;
|
||||
int iterations = 0;
|
||||
while (totalHeight > tableHeight - ZoneSizes::MINIMUM_STACK_HEIGHT && iterations < MAX_MINIMIZE_ITERATIONS) {
|
||||
if (!expandedZone->tryMinimizeAbove())
|
||||
break;
|
||||
|
||||
totalHeight = totalCommandZoneHeight();
|
||||
++iterations;
|
||||
}
|
||||
|
||||
rearrangeZones();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::setCommandZonesVisible(bool visible)
|
||||
{
|
||||
if (commandZoneGraphicsItem) {
|
||||
commandZoneGraphicsItem->setVisible(visible);
|
||||
}
|
||||
|
||||
// Reset partner zone to collapsed state when hiding
|
||||
if (!visible && partnerZoneGraphicsItem) {
|
||||
partnerZoneGraphicsItem->setExpanded(false);
|
||||
}
|
||||
rearrangeZones();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::setCompanionZoneVisible(bool visible)
|
||||
{
|
||||
if (companionZoneGraphicsItem) {
|
||||
companionZoneGraphicsItem->setVisible(visible);
|
||||
if (!visible) {
|
||||
companionZoneGraphicsItem->setExpanded(false);
|
||||
}
|
||||
}
|
||||
rearrangeZones();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::setBackgroundZoneVisible(bool visible)
|
||||
{
|
||||
if (backgroundZoneGraphicsItem) {
|
||||
backgroundZoneGraphicsItem->setVisible(visible);
|
||||
if (!visible) {
|
||||
backgroundZoneGraphicsItem->setExpanded(false);
|
||||
}
|
||||
}
|
||||
rearrangeZones();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file player_graphics_item.h
|
||||
* @ingroup GameGraphicsPlayers
|
||||
* @brief TODO: Document this.
|
||||
* @brief Root QGraphicsObject container that owns and lays out all visual game elements for a single player.
|
||||
*/
|
||||
|
||||
#ifndef COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <QGraphicsObject>
|
||||
|
||||
class CommandZone;
|
||||
class HandZone;
|
||||
class PileZone;
|
||||
class PlayerTarget;
|
||||
|
|
@ -18,6 +19,26 @@ class StackZone;
|
|||
class TableZone;
|
||||
class ZoneViewZone;
|
||||
|
||||
/**
|
||||
* The top-level graphics item representing a single player's entire board state in the game scene.
|
||||
*
|
||||
* PlayerGraphicsItem owns and manages every zone graphics item belonging to one player:
|
||||
* the table (battlefield), hand, deck, graveyard, removed-from-game, sideboard, stack,
|
||||
* and command/partner zones. It also owns the player avatar target widget, the background
|
||||
* PlayerArea, and the hand card counter.
|
||||
*
|
||||
* Layout responsibilities include:
|
||||
* - Positioning all zones relative to one another based on hand orientation
|
||||
* (horizontal or vertical) and whether the view is mirrored (opponent's perspective).
|
||||
* - Responding to scene width changes by resizing the table and hand zones proportionally.
|
||||
* - Managing the 55-pixel left counter strip where life totals and other counters are displayed.
|
||||
* - Showing or hiding command/partner zones when the server signals Commander format support.
|
||||
*
|
||||
* This item emits sizeChanged() whenever its bounding rect changes, which GameScene uses
|
||||
* to reflow the positions of all players in the scene.
|
||||
*
|
||||
* @see PlayerArea, PlayerTarget, TableZone, HandZone, PileZone, StackZone, CommandZone
|
||||
*/
|
||||
class PlayerGraphicsItem : public QGraphicsObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -99,10 +120,29 @@ public:
|
|||
{
|
||||
return handZoneGraphicsItem;
|
||||
}
|
||||
[[nodiscard]] CommandZone *getCommandZoneGraphicsItem() const
|
||||
{
|
||||
return commandZoneGraphicsItem;
|
||||
}
|
||||
[[nodiscard]] CommandZone *getPartnerZoneGraphicsItem() const
|
||||
{
|
||||
return partnerZoneGraphicsItem;
|
||||
}
|
||||
[[nodiscard]] CommandZone *getCompanionZoneGraphicsItem() const
|
||||
{
|
||||
return companionZoneGraphicsItem;
|
||||
}
|
||||
[[nodiscard]] CommandZone *getBackgroundZoneGraphicsItem() const
|
||||
{
|
||||
return backgroundZoneGraphicsItem;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void onPlayerActiveChanged(bool _active);
|
||||
void retranslateUi();
|
||||
void setCommandZonesVisible(bool visible);
|
||||
void setCompanionZoneVisible(bool visible);
|
||||
void setBackgroundZoneVisible(bool visible);
|
||||
|
||||
signals:
|
||||
void sizeChanged();
|
||||
|
|
@ -119,13 +159,53 @@ private:
|
|||
TableZone *tableZoneGraphicsItem;
|
||||
StackZone *stackZoneGraphicsItem;
|
||||
HandZone *handZoneGraphicsItem;
|
||||
CommandZone *commandZoneGraphicsItem;
|
||||
CommandZone *partnerZoneGraphicsItem;
|
||||
CommandZone *companionZoneGraphicsItem;
|
||||
CommandZone *backgroundZoneGraphicsItem;
|
||||
QRectF bRect;
|
||||
bool mirrored;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Positions command zones and calculates stack zone offset.
|
||||
*
|
||||
* This helper method centralizes the command zone positioning logic that
|
||||
* was previously duplicated across multiple layout branches. It positions
|
||||
* the command zone at the given base position, positions the partner zone
|
||||
* relative to it, and returns the Y offset where the stack zone should start.
|
||||
*
|
||||
* @param base The position for the command zone
|
||||
* @param commandZonesVisible Whether command zones are enabled
|
||||
* @return Y offset from base where stack zone should be positioned
|
||||
*/
|
||||
qreal positionCommandZones(const QPointF &base, bool commandZonesVisible);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Calculates the total height of all visible command zones.
|
||||
*
|
||||
* Sums the current heights of the primary command zone plus any
|
||||
* expanded partner, companion, and background zones.
|
||||
*
|
||||
* @return Total height in pixels of all visible command zones
|
||||
*/
|
||||
[[nodiscard]] qreal totalCommandZoneHeight() const;
|
||||
|
||||
private slots:
|
||||
void updateBoundingRect();
|
||||
void rearrangeZones();
|
||||
void rearrangeCounters();
|
||||
/**
|
||||
* @brief Handles zone expansion, minimizing the zone above if space is needed.
|
||||
*
|
||||
* Uses sender() to identify which zone triggered the expansion. When a zone
|
||||
* expands and there isn't enough vertical space, this slot calls tryMinimizeAbove()
|
||||
* to minimize the zone above instead of the newly-opened zone.
|
||||
*
|
||||
* @param expanded True if the zone is expanding, false if collapsing
|
||||
*/
|
||||
void onZoneExpanded(bool expanded);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue