Add moveToTable context menu action and extract tableRowToGridY helper (#6738)

Adds a Table option to the Move menu, allowing cards to be moved directly to the battlefield from any zone. Extracts the repeated tableRow-to-grid-Y conversion logic into TableZone::tableRowToGridY(), consolidating five call sites and fixing a latent bug where cards with tableRow > 2 could land on the wrong row.
This commit is contained in:
DawnFire42 2026-03-24 16:45:52 -04:00 committed by GitHub
parent 70b41c2095
commit 94ea574c76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 64 additions and 14 deletions

View file

@ -563,6 +563,9 @@ private:
{"Player/aMoveToTopLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aMoveToTable", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aViewHand",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Hand"), parseSequenceString(""), ShortcutGroup::View)},
{"Player/aViewGraveyard",

View file

@ -9,17 +9,20 @@
enum CardMenuActionType
{
// Per-card attribute actions (must be <= cmClone for cardMenuAction() dispatch)
cmTap,
cmUntap,
cmDoesntUntap,
cmFlip,
cmPeek,
cmClone,
// Move actions (must be > cmClone for cardMenuAction() dispatch)
cmMoveToTopLibrary,
cmMoveToBottomLibrary,
cmMoveToHand,
cmMoveToGraveyard,
cmMoveToExile
cmMoveToExile,
cmMoveToTable
};
#endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H

View file

@ -11,6 +11,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to"))
aMoveToBottomLibrary = new QAction(this);
aMoveToBottomLibrary->setData(cmMoveToBottomLibrary);
aMoveToXfromTopOfLibrary = new QAction(this);
aMoveToTable = new QAction(this);
aMoveToTable->setData(cmMoveToTable);
aMoveToGraveyard = new QAction(this);
aMoveToHand = new QAction(this);
aMoveToHand->setData(cmMoveToHand);
@ -22,6 +24,7 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to"))
connect(aMoveToBottomLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToXfromTopOfLibrary, &QAction::triggered, player->getPlayerActions(),
&PlayerActions::actMoveCardXCardsFromTop);
connect(aMoveToTable, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToHand, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToGraveyard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToExile, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
@ -30,6 +33,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to"))
addAction(aMoveToXfromTopOfLibrary);
addAction(aMoveToBottomLibrary);
addSeparator();
addAction(aMoveToTable);
addSeparator();
addAction(aMoveToHand);
addSeparator();
addAction(aMoveToGraveyard);
@ -47,6 +52,7 @@ void MoveMenu::setShortcutsActive()
aMoveToTopLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToTopLibrary"));
aMoveToBottomLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToBottomLibrary"));
aMoveToTable->setShortcuts(shortcuts.getShortcut("Player/aMoveToTable"));
aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand"));
aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard"));
aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile"));
@ -57,7 +63,8 @@ void MoveMenu::retranslateUi()
aMoveToTopLibrary->setText(tr("&Top of library in random order"));
aMoveToXfromTopOfLibrary->setText(tr("X cards from the top of library..."));
aMoveToBottomLibrary->setText(tr("&Bottom of library in random order"));
aMoveToTable->setText(tr("T&able"));
aMoveToHand->setText(tr("&Hand"));
aMoveToGraveyard->setText(tr("&Graveyard"));
aMoveToExile->setText(tr("&Exile"));
}
}

View file

@ -23,6 +23,7 @@ public:
QAction *aMoveToBottomLibrary = nullptr;
QAction *aMoveToHand = nullptr;
QAction *aMoveToTable = nullptr;
QAction *aMoveToGraveyard = nullptr;
QAction *aMoveToExile = nullptr;
};

View file

@ -75,7 +75,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
cmd.set_y(0);
} else {
tableRow = faceDown ? 2 : info.getUiAttributes().tableRow;
QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow));
cardToMove->set_face_down(faceDown);
if (!faceDown) {
cardToMove->set_pt(info.getPowTough().toStdString());
@ -114,12 +114,7 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown)
const CardInfo &info = exactCard.getInfo();
int tableRow = faceDown ? 2 : info.getUiAttributes().tableRow;
// default instant/sorcery cards to the noncreatures row
if (tableRow > 2) {
tableRow = 1;
}
QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow));
cardToMove->set_face_down(faceDown);
if (!faceDown) {
cardToMove->set_pt(info.getPowTough().toStdString());
@ -866,7 +861,7 @@ void PlayerActions::actCreateToken()
ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId});
if (correctedCard) {
lastTokenInfo.name = correctedCard.getName();
lastTokenTableRow = TableZone::clampValidTableRow(2 - correctedCard.getInfo().getUiAttributes().tableRow);
lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow);
if (lastTokenInfo.pt.isEmpty()) {
lastTokenInfo.pt = correctedCard.getInfo().getPowTough();
}
@ -917,7 +912,7 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo)
.providerId =
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
lastTokenTableRow = TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow);
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
utilityMenu->setAndEnableCreateAnotherTokenAction(tr("C&reate another %1 token").arg(lastTokenInfo.name));
}
@ -1085,9 +1080,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
return;
}
// get the target token's location
// TODO: Define this QPoint into its own function along with the one below
QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow));
// create the token for the related card
Command_CreateToken cmd;
@ -1930,6 +1923,34 @@ void PlayerActions::cardMenuAction()
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) {
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(ZoneNames::TABLE);
cmd->set_x(-1);
CardToMove *ctm = cmd->mutable_cards_to_move()->add_card();
ctm->set_card_id(card->getId());
ctm->set_face_down(false);
int tableRow = 0;
ExactCard exactCard = card->getCard();
if (exactCard) {
const CardInfo &info = exactCard.getInfo();
tableRow = info.getUiAttributes().tableRow;
ctm->set_pt(info.getPowTough().toStdString());
ctm->set_tapped(info.getUiAttributes().cipt);
}
cmd->set_y(TableZone::tableRowToGridY(tableRow));
commandList.append(cmd);
}
break;
}
default:
break;
}

View file

@ -382,3 +382,11 @@ int TableZone::clampValidTableRow(const int row)
return TABLEROWS - 1;
return row;
}
int TableZone::tableRowToGridY(int tableRow)
{
if (tableRow > 2) {
tableRow = 1;
}
return clampValidTableRow(2 - tableRow);
}

View file

@ -151,6 +151,13 @@ public:
static int clampValidTableRow(const int row);
/**
* Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands)
* to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g.,
* instants/sorceries) default to the noncreatures row.
*/
static int tableRowToGridY(int tableRow);
/**
Resizes the TableZone in case CardItems are within or
outside of the TableZone constraints.