This commit is contained in:
Bryce! 2026-11-04 15:54:56 -05:00 • committed by GitHub
commit 7e7f283cab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 234 additions and 29 deletions

View file

@ -86,6 +86,7 @@ set(cockatrice_SOURCES
src/game_graphics/deckview/tabbed_deck_view_container.cpp
src/game_graphics/dialogs/dlg_annotation.cpp
src/game_graphics/dialogs/dlg_create_token.cpp
src/game_graphics/dialogs/token_preview_widget.cpp
src/game_graphics/dialogs/dlg_move_top_cards_until.cpp
src/game_graphics/dialogs/dlg_roll_dice.cpp
src/game_graphics/game_scene.cpp

View file

@ -1,9 +1,9 @@
#include "dlg_create_token.h"
#include "../../client/settings/cache_settings.h"
#include "../../interface/widgets/cards/card_info_picture_widget.h"
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "token_preview_widget.h"
#include <QByteArray>
#include <QChar>
@ -20,6 +20,7 @@
#include <QModelIndex>
#include <QRadioButton>
#include <QSet>
#include <QSignalBlocker>
#include <QTimer>
#include <QTreeView>
#include <QVBoxLayout>
@ -41,7 +42,7 @@ class QWidget;
DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent)
: QDialog(parent), predefinedTokens(_predefinedTokens)
{
pic = new CardInfoPictureWidget();
pic = new TokenPreviewWidget();
pic->setObjectName("pic");
nameLabel = new QLabel(tr("&Name:"));
@ -173,6 +174,12 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
setWindowTitle(tr("Create token"));
connect(nameEdit, &QLineEdit::textChanged, this, &DlgCreateToken::updatePreview);
connect(colorEdit, qOverload<int>(&QComboBox::currentIndexChanged), this, &DlgCreateToken::updatePreview);
connect(ptEdit, &QLineEdit::textChanged, this, &DlgCreateToken::updatePreview);
connect(annotationEdit, &QLineEdit::textChanged, this, &DlgCreateToken::updatePreview);
updatePreview();
resize(600, 500);
restoreGeometry(SettingsCache::instance().layouts().getTokenDialogGeometry());
}
@ -185,17 +192,14 @@ void DlgCreateToken::closeEvent(QCloseEvent *event)
void DlgCreateToken::faceDownCheckBoxToggled(bool checked)
{
// Keep the face-up draft visible while choosing what to create. Only the
// outgoing command applies face-down color/P/T restrictions.
colorEdit->setEnabled(!checked);
ptEdit->setEnabled(!checked);
if (checked) {
colorEdit->setCurrentIndex(6);
colorEdit->setEnabled(false);
ptEdit->clear();
ptEdit->clearFocus();
ptEdit->setEnabled(false);
} else {
colorEdit->setEnabled(true);
ptEdit->setEnabled(true);
annotationEdit->setEnabled(true);
}
updatePreview();
}
void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QModelIndex & /*previous*/)
@ -204,11 +208,14 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QMo
CardInfoPtr cardInfo;
if (current.row() >= 0) {
if (realIndex.isValid()) {
cardInfo = cardDatabaseModel->getCard(realIndex.row());
}
if (cardInfo) {
const QSignalBlocker colorBlocker(colorEdit);
const QSignalBlocker ptBlocker(ptEdit);
const QSignalBlocker annotationBlocker(annotationEdit);
updateSearchFieldWithoutUpdatingFilter(cardInfo->getName());
const QChar cardColor = cardInfo->getColorChar();
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
@ -216,21 +223,22 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QMo
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
annotationEdit->setText(cardInfo->getText());
}
} else {
nameEdit->setText("");
colorEdit->setCurrentIndex(colorEdit->findData(QString(), Qt::UserRole, Qt::MatchFixedString));
ptEdit->setText("");
annotationEdit->setText("");
}
pic->setCard(CardDatabaseManager::query()->getPreferredCard(cardInfo));
// Filtering, changing the deck/all scope, or reloading the database can
// invalidate the current row. None of these actions discard the token draft.
updatePreview();
}
void DlgCreateToken::updateSearchFieldWithoutUpdatingFilter(const QString &newValue) const
{
nameEdit->blockSignals(true);
const QSignalBlocker blocker(nameEdit);
nameEdit->setText(newValue);
nameEdit->blockSignals(false);
}
void DlgCreateToken::updatePreview()
{
pic->setToken(tokenDraft());
}
void DlgCreateToken::updateSearch(const QString &search)
@ -264,13 +272,35 @@ void DlgCreateToken::actReject()
reject();
}
TokenInfo DlgCreateToken::tokenDraft() const
{
TokenInfo token = {.name = nameEdit->text(),
.color = colorEdit->itemData(colorEdit->currentIndex()).toString(),
.pt = ptEdit->text(),
.annotation = annotationEdit->text(),
.destroy = destroyCheckBox->isChecked(),
.faceDown = faceDownCheckBox->isChecked(),
.providerId =
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(nameEdit->text())};
// Match the normalization applied when the player creates the token, so
// manually typed aliases and the default P/T preview the actual result.
const ExactCard correctedCard = CardDatabaseManager::query()->guessCard({token.name, token.providerId});
if (correctedCard) {
token.name = correctedCard.getName();
if (token.pt.isEmpty()) {
token.pt = correctedCard.getInfo().getPowTough();
}
}
return token;
}
TokenInfo DlgCreateToken::getTokenInfo() const
{
return {.name = nameEdit->text(),
.color = colorEdit->itemData(colorEdit->currentIndex()).toString(),
.pt = ptEdit->text(),
.annotation = annotationEdit->text(),
.destroy = destroyCheckBox->isChecked(),
.faceDown = faceDownCheckBox->isChecked(),
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(nameEdit->text())};
TokenInfo token = tokenDraft();
if (token.faceDown) {
token.color = QString(QChar());
token.pt.clear();
}
return token;
}

View file

@ -22,7 +22,7 @@ class QRadioButton;
class QTreeView;
class CardDatabaseModel;
class TokenDisplayModel;
class CardInfoPictureWidget;
class TokenPreviewWidget;
class QModelIndex;
class QWidget;
@ -39,6 +39,7 @@ private slots:
void faceDownCheckBoxToggled(bool checked);
void tokenSelectionChanged(const QModelIndex &current, const QModelIndex &previous);
void updateSearch(const QString &search);
void updatePreview();
void actChooseTokenFromAll(bool checked);
void actChooseTokenFromDeck(bool checked);
void actOk();
@ -54,9 +55,10 @@ private:
QCheckBox *destroyCheckBox;
QCheckBox *faceDownCheckBox;
QRadioButton *chooseTokenFromAllRadioButton, *chooseTokenFromDeckRadioButton;
CardInfoPictureWidget *pic;
TokenPreviewWidget *pic;
QTreeView *chooseTokenView;
[[nodiscard]] TokenInfo tokenDraft() const;
void updateSearchFieldWithoutUpdatingFilter(const QString &newValue) const;
};

View file

@ -0,0 +1,85 @@
#include "token_preview_widget.h"
#include "../board/abstract_card_item.h"
#include <QGraphicsScene>
#include <QPainter>
#include <QResizeEvent>
#include <libcockatrice/card/card_info.h>
class TokenPreviewCardItem : public AbstractCardItem
{
public:
TokenPreviewCardItem()
{
setFlag(ItemIsSelectable, false);
setAcceptedMouseButtons(Qt::NoButton);
setAcceptHoverEvents(false);
setCursor(Qt::ArrowCursor);
connect(state, &CardState::stateChanged, this, [this] { update(); });
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
// Show what the user is creating even when it will enter face down.
// Reuse the board's artwork, custom-token background and name rendering.
AbstractCardItem::paint(painter, option, widget);
const QSizeF translatedSize = getTranslatedSize(painter);
const qreal scale = translatedSize.width() / boundingRect().width();
painter->save();
transformPainter(painter, translatedSize, 0);
painter->setPen(Qt::white);
painter->setBackground(Qt::black);
painter->setBackgroundMode(Qt::OpaqueMode);
const QRectF textRect(4 * scale, 4 * scale, translatedSize.width() - 8 * scale,
translatedSize.height() - 8 * scale);
painter->drawText(textRect, Qt::AlignCenter | Qt::TextWrapAnywhere, state->getAnnotation());
if (state->getPT() != getCardInfo().getPowTough()) {
painter->setPen(QColor(255, 150, 0));
}
painter->drawText(textRect, Qt::AlignRight | Qt::AlignBottom, state->getPT());
painter->restore();
}
};
TokenPreviewWidget::TokenPreviewWidget(QWidget *parent) : QGraphicsView(parent), card(new TokenPreviewCardItem)
{
auto *previewScene = new QGraphicsScene(this);
previewScene->addItem(card);
previewScene->setSceneRect(card->boundingRect());
setScene(previewScene);
setInteractive(false);
setFocusPolicy(Qt::NoFocus);
setFrameShape(QFrame::NoFrame);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setMinimumSize(150, 200);
setAccessibleName(tr("Token preview"));
}
void TokenPreviewWidget::setToken(const TokenInfo &token)
{
card->setCardRef({token.name, token.providerId});
card->setColor(token.color);
card->setFaceDown(false);
card->getState()->setPT(token.pt);
card->getState()->setAnnotation(token.annotation);
setToolTip(token.annotation);
QString description = token.name + " " + token.pt + " " + token.annotation;
if (token.faceDown) {
description += " " + tr("Will be created face down.");
}
setAccessibleDescription(description);
}
QSize TokenPreviewWidget::sizeHint() const
{
return {200, 280};
}
void TokenPreviewWidget::resizeEvent(QResizeEvent *event)
{
QGraphicsView::resizeEvent(event);
fitInView(sceneRect(), Qt::KeepAspectRatio);
}

View file

@ -0,0 +1,25 @@
#ifndef COCKATRICE_TOKEN_PREVIEW_WIDGET_H
#define COCKATRICE_TOKEN_PREVIEW_WIDGET_H
#include <QGraphicsView>
#include <libcockatrice/game/player/token_info.h>
class TokenPreviewCardItem;
/** A noninteractive preview of the token that the dialog will create. */
class TokenPreviewWidget : public QGraphicsView
{
Q_OBJECT
public:
explicit TokenPreviewWidget(QWidget *parent = nullptr);
void setToken(const TokenInfo &token);
QSize sizeHint() const override;
protected:
void resizeEvent(QResizeEvent *event) override;
private:
TokenPreviewCardItem *card;
};
#endif

View file

@ -951,7 +951,7 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate)
lastTokenInfo.name = correctedCard.getName();
int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow;
lastTokenTableRow = TableZoneLogic::tableRowToGridY(tableRow);
if (lastTokenInfo.pt.isEmpty()) {
if (!lastTokenInfo.faceDown && lastTokenInfo.pt.isEmpty()) {
lastTokenInfo.pt = correctedCard.getInfo().getPowTough();
}
}

View file

@ -4,6 +4,7 @@
#include <QString>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/zones/table_zone_logic.h>
#include <libcockatrice/protocol/pb/command_create_token.pb.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/utility/zone_names.h>
#include <string>
@ -117,3 +118,64 @@ TEST(PlayerActionsTest, PlayCiptCreatureSendsTapped)
EXPECT_TRUE(card.has_tapped());
EXPECT_TRUE(card.tapped());
}
// These three #7737 command cases use the current shared game harness.
TEST(TokenCommandTest, FaceDownKnownTokenDoesNotRestoreDatabasePowerToughness)
{
GameHarness harness;
harness.client()->sent.clear();
TokenInfo token;
token.name = QStringLiteral("Kitten");
token.color = QString(QChar());
token.faceDown = true;
harness.player()->getPlayerActions()->actCreateToken(token);
ASSERT_EQ(harness.client()->sent.size(), 1);
ASSERT_EQ(harness.lastCommand().game_command_size(), 1);
const auto &gameCommand = harness.lastCommand().game_command(0);
ASSERT_TRUE(gameCommand.HasExtension(Command_CreateToken::ext));
const auto &command = gameCommand.GetExtension(Command_CreateToken::ext);
EXPECT_EQ(command.card_name(), "Kitten");
EXPECT_TRUE(command.face_down());
EXPECT_TRUE(command.pt().empty());
harness.player()->getPlayerActions()->actCreateAnotherToken();
ASSERT_EQ(harness.client()->sent.size(), 2);
const auto &repeat = harness.lastCommand().game_command(0).GetExtension(Command_CreateToken::ext);
EXPECT_TRUE(repeat.face_down());
EXPECT_TRUE(repeat.pt().empty());
}
TEST(TokenCommandTest, FaceUpKnownAliasKeepsCanonicalNameAndDefaultPowerToughness)
{
GameHarness harness;
harness.client()->sent.clear();
TokenInfo token;
token.name = QStringLiteral("kitten");
harness.player()->getPlayerActions()->actCreateToken(token);
ASSERT_EQ(harness.client()->sent.size(), 1);
ASSERT_EQ(harness.lastCommand().game_command_size(), 1);
const auto &gameCommand = harness.lastCommand().game_command(0);
ASSERT_TRUE(gameCommand.HasExtension(Command_CreateToken::ext));
const auto &command = gameCommand.GetExtension(Command_CreateToken::ext);
EXPECT_EQ(command.card_name(), "Kitten");
EXPECT_FALSE(command.face_down());
EXPECT_EQ(command.pt(), "1/1");
}
TEST(TokenCommandTest, FaceUpKnownTokenPreservesExplicitPowerToughness)
{
GameHarness harness;
harness.client()->sent.clear();
TokenInfo token;
token.name = QStringLiteral("Kitten");
token.pt = QStringLiteral("7/9");
token.annotation = QStringLiteral("Flying");
harness.player()->getPlayerActions()->actCreateToken(token);
ASSERT_EQ(harness.client()->sent.size(), 1);
ASSERT_EQ(harness.lastCommand().game_command_size(), 1);
const auto &gameCommand = harness.lastCommand().game_command(0);
ASSERT_TRUE(gameCommand.HasExtension(Command_CreateToken::ext));
const auto &command = gameCommand.GetExtension(Command_CreateToken::ext);
EXPECT_FALSE(command.face_down());
EXPECT_EQ(command.pt(), "7/9");
EXPECT_EQ(command.annotation(), "Flying");
}