diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1ca3c77c2..6266777b4 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -276,6 +276,13 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index a66897b4a..ae925935c 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -1,5 +1,6 @@ #include "cache_settings.h" +#include "../../interface/widgets/dialogs/dlg_settings.h" #include "../network/update/client/release_channel.h" #include "card_counter_settings.h" #include "version_string.h" @@ -302,6 +303,14 @@ SettingsCache::SettingsCache() deckEditorBannerCardComboBoxVisible = settings->value("interface/deckeditorbannercardcomboboxvisible", true).toBool(); deckEditorTagsWidgetVisible = settings->value("interface/deckeditortagswidgetvisible", true).toBool(); + deckEditorCommanderSpellbookIntegrationEnabled = + settings + ->value("interface/deck_editor/commander_spellbook_integration/enabled", + deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) + .toInt(); + deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames = + settings->value("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names", false) + .toBool(); visualDeckStorageCardSize = settings->value("interface/visualdeckstoragecardsize", 100).toInt(); visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt(); visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool(); @@ -798,6 +807,26 @@ void SettingsCache::setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEdito emit deckEditorTagsWidgetVisibleChanged(deckEditorTagsWidgetVisible); } +void SettingsCache::setDeckEditorCommanderSpellbookIntegrationEnabled( + int _deckEditorCommanderSpellbookIntegrationEnabled) +{ + deckEditorCommanderSpellbookIntegrationEnabled = _deckEditorCommanderSpellbookIntegrationEnabled; + settings->setValue("interface/deck_editor/commander_spellbook_integration/enabled", + deckEditorCommanderSpellbookIntegrationEnabled); + emit deckEditorCommanderSpellbookIntegrationEnabledChanged(deckEditorCommanderSpellbookIntegrationEnabled); +} + +void SettingsCache::setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames( + bool _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames) +{ + deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames = + _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames; + settings->setValue("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names", + deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames); + emit deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged( + deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames); +} + void SettingsCache::setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder) { visualDeckStorageSortingOrder = _visualDeckStorageSortingOrder; diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index ece61487f..d6a33f2e7 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -156,6 +156,8 @@ signals: void printingSelectorNavigationButtonsVisibleChanged(); void deckEditorBannerCardComboBoxVisibleChanged(bool _visible); void deckEditorTagsWidgetVisibleChanged(bool _visible); + void deckEditorCommanderSpellbookIntegrationEnabledChanged(int _enabled); + void deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool _useOfficialBracketNames); void visualDeckStorageShowTagFilterChanged(bool _visible); void visualDeckStorageDefaultTagsListChanged(); void visualDeckStorageShowColorIdentityChanged(bool _visible); @@ -246,6 +248,8 @@ private: bool printingSelectorNavigationButtonsVisible; bool deckEditorBannerCardComboBoxVisible; bool deckEditorTagsWidgetVisible; + int deckEditorCommanderSpellbookIntegrationEnabled; + bool deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames; int visualDeckStorageSortingOrder; bool visualDeckStorageShowFolders; bool visualDeckStorageShowColorIdentity; @@ -719,6 +723,14 @@ public: { return openDeckInNewTab; } + [[nodiscard]] int getDeckEditorCommanderSpellbookIntegrationEnabled() const + { + return deckEditorCommanderSpellbookIntegrationEnabled; + } + [[nodiscard]] bool getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() const + { + return deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames; + } [[nodiscard]] int getRewindBufferingMs() const { return rewindBufferingMs; @@ -1050,6 +1062,9 @@ public slots: void setPrintingSelectorNavigationButtonsVisible(QT_STATE_CHANGED_T _navigationButtonsVisible); void setDeckEditorBannerCardComboBoxVisible(QT_STATE_CHANGED_T _deckEditorBannerCardComboBoxVisible); void setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEditorTagsWidgetVisible); + void setDeckEditorCommanderSpellbookIntegrationEnabled(int _deckEditorCommanderSpellbookIntegrationEnabled); + void setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames( + bool _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames); void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder); void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value); void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index f939ae99d..f67f80296 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -1,13 +1,19 @@ #include "deck_editor_deck_dock_widget.h" #include "../../../client/settings/cache_settings.h" +#include "../dialogs/dlg_settings.h" +#include "../tabs/api/commander_spellbook/commander_spellbook_api_accessor.h" +#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h" #include "deck_list_style_proxy.h" #include "deck_state_manager.h" #include +#include #include +#include #include #include +#include #include #include #include @@ -131,6 +137,37 @@ void DeckEditorDeckDockWidget::createDeckDock() formatComboBox->addItem(tr("Loading Database...")); formatComboBox->setEnabled(false); // Disable until loaded + // --- Commander bracket row (hidden, unless format is 'commander') --- + bracketLabel = new QLabel(tr("Bracket:"), this); + + bracketValueLabel = new QLabel(this); + bracketValueLabel->setText("-"); + bracketValueLabel->setObjectName("bracketValueLabel"); + + bracketInfoButton = new QToolButton(this); + bracketInfoButton->setText("?"); + bracketInfoButton->setAutoRaise(true); + bracketInfoButton->setEnabled(false); + + bracketRefreshButton = new QToolButton(this); + bracketRefreshButton->setIcon(QPixmap("theme:icons/reload")); + bracketRefreshButton->setAutoRaise(true); + + connect(bracketRefreshButton, &QToolButton::clicked, this, &DeckEditorDeckDockWidget::requestBracketEstimate); + if (SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled() != + deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) { + connect(&SettingsCache::instance(), &SettingsCache::deckEditorCommanderSpellbookIntegrationEnabledChanged, this, + &DeckEditorDeckDockWidget::maybeAutoEstimateBracket); + connect(&SettingsCache::instance(), + &SettingsCache::deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged, this, + &DeckEditorDeckDockWidget::maybeAutoEstimateBracket); + } + + bracketLabel->setVisible(false); + bracketValueLabel->setVisible(false); + bracketInfoButton->setVisible(false); + bracketRefreshButton->setVisible(false); + commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; @@ -216,13 +253,23 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(formatLabel, 2, 0); upperLayout->addWidget(formatComboBox, 2, 1); - upperLayout->addWidget(bannerCardLabel, 3, 0); - upperLayout->addWidget(bannerCardComboBox, 3, 1); + upperLayout->addWidget(bracketLabel, 3, 0); - upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); + auto *bracketRow = new QHBoxLayout; + bracketRow->addWidget(bracketValueLabel); + bracketRow->addWidget(bracketInfoButton); + bracketRow->addWidget(bracketRefreshButton); + bracketRow->addStretch(); - upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); - upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1); + upperLayout->addLayout(bracketRow, 3, 1); + + upperLayout->addWidget(bannerCardLabel, 4, 0); + upperLayout->addWidget(bannerCardComboBox, 4, 1); + + upperLayout->addWidget(deckTagsDisplayWidget, 5, 1); + + upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1); hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); @@ -280,6 +327,151 @@ void DeckEditorDeckDockWidget::createDeckDock() } } +bool DeckEditorDeckDockWidget::promptCommanderSpellbookIntegration() +{ + QDialog dialog(this); + dialog.setWindowTitle(tr("CommanderSpellbook integration")); + + auto *mainLayout = new QVBoxLayout(&dialog); + + // Main text + auto *label = new QLabel(tr("CommanderSpellbook can analyze your deck and estimate its Commander bracket.\n\n" + "This sends your deck list to an external service.\n\n" + "CommanderSpellbook uses its own bracket naming system based on their own algorithm. " + "These names can be mapped to the official Commander brackets, but the mapping " + "is only an approximation.")); + label->setWordWrap(true); + mainLayout->addWidget(label); + + // Naming selector + auto *formLayout = new QFormLayout; + auto *namingCombo = new QComboBox(&dialog); + namingCombo->addItem(tr("CommanderSpellbook bracket names")); + namingCombo->addItem(tr("Official Commander bracket names (approximate)")); + namingCombo->setCurrentIndex( + SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + // Create label + explainer button + auto *labelWidget = new QWidget(&dialog); + auto *labelLayout = new QHBoxLayout(labelWidget); + labelLayout->setContentsMargins(0, 0, 0, 0); + + auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget); + auto *explainerButton = new QToolButton(labelWidget); + explainerButton->setText("?"); + explainerButton->setAutoRaise(true); + explainerButton->setEnabled(false); + explainerButton->setToolTip(CommanderBracketNames::Explainer); + + labelLayout->addWidget(namingLabel); + labelLayout->addWidget(explainerButton); + labelLayout->addStretch(); // push the button next to label, combo stays aligned + + // Add row with the custom label widget + formLayout->addRow(labelWidget, namingCombo); + mainLayout->addLayout(formLayout); + + // Buttons + auto *buttonBox = new QDialogButtonBox(&dialog); + auto *enableBtn = buttonBox->addButton(tr("Enable"), QDialogButtonBox::AcceptRole); + auto *automaticBtn = buttonBox->addButton(tr("Automatic"), QDialogButtonBox::ApplyRole); + auto *disableBtn = buttonBox->addButton(tr("Disable"), QDialogButtonBox::RejectRole); + mainLayout->addWidget(buttonBox); + + // Track which button was clicked + QAbstractButton *clickedButton = nullptr; + QObject::connect(buttonBox, &QDialogButtonBox::clicked, &dialog, [&](QAbstractButton *btn) { + clickedButton = btn; + dialog.accept(); + }); + + dialog.exec(); + + // Persist naming choice (if not disabled) + if (clickedButton != disableBtn) { + bool useOfficial = namingCombo->currentIndex() == 1; + SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial); + } + + connect(&SettingsCache::instance(), &SettingsCache::deckEditorCommanderSpellbookIntegrationEnabledChanged, this, + &DeckEditorDeckDockWidget::maybeAutoEstimateBracket); + connect(&SettingsCache::instance(), + &SettingsCache::deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged, this, + &DeckEditorDeckDockWidget::maybeAutoEstimateBracket); + + // Persist integration mode + if (clickedButton == disableBtn) { + SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled( + deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled); + return false; + } + if (clickedButton == enableBtn) { + SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled( + deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled); + return true; + } + if (clickedButton == automaticBtn) { + SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled( + deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic); + return true; + } + + return false; +} + +void DeckEditorDeckDockWidget::updateBracketVisibility(bool visible) +{ + bracketLabel->setVisible(visible); + bracketValueLabel->setVisible(visible); + bracketInfoButton->setVisible(visible); + bracketRefreshButton->setVisible(visible); +} + +void DeckEditorDeckDockWidget::requestBracketEstimate() +{ + bracketRefreshButton->setEnabled(false); + bracketInfoButton->setEnabled(false); + bracketValueLabel->setText(tr("Calculating…")); + + requestId = CommanderSpellbookApiAccessor::instance().estimateBracket(*deckStateManager->getModel()->getDeckList(), this); + + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this, + &DeckEditorDeckDockWidget::onEstimateBracketFinished); +} + +void DeckEditorDeckDockWidget::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result) +{ + if (requester != this || static_cast(id) != requestId) { + return; + } + + BracketExplainer explainer; + lastBracketExplanation = explainer.explain(result); + + // Display bracket + if (SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames()) { + bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToOfficialString(result.bracketTag)); + } else { + bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToString(result.bracketTag)); + } + bracketRefreshButton->setEnabled(true); + + // Build tooltip + QString tooltip; + for (const auto §ion : lastBracketExplanation.sections) { + tooltip += "" + section.title + "
"; + for (const auto &line : section.bulletPoints) { + tooltip += "• " + line + "
"; + } + tooltip += "
"; + } + + bracketInfoButton->setToolTip(tooltip); + bracketInfoButton->setEnabled(!tooltip.isEmpty()); +} + void DeckEditorDeckDockWidget::initializeFormats() { QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys(); @@ -300,15 +492,70 @@ void DeckEditorDeckDockWidget::initializeFormats() // Ensure no selection is visible initially formatComboBox->setCurrentIndex(-1); } - connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + QString formatKey; if (index >= 0) { QString formatKey = formatComboBox->itemData(index).toString(); deckStateManager->setFormat(formatKey); } else { deckStateManager->setFormat(""); // clear format if deselected } + + const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0); + const bool commanderSpellbookIntegrationEnabled = + SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled() != + deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled; + + const bool bracketVisible = isCommander && commanderSpellbookIntegrationEnabled; + + updateBracketVisibility(bracketVisible); + + if (!isCommander) { + bracketValueLabel->setText("-"); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); + bracketRefreshButton->setEnabled(false); + } else { + bracketRefreshButton->setEnabled(true); + maybeAutoEstimateBracket(); + } }); + + maybeAutoEstimateBracket(); +} + +void DeckEditorDeckDockWidget::maybeAutoEstimateBracket() +{ + const QString formatKey = deckStateManager->getModel()->getDeckList()->getGameFormat(); + + const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0); + + int mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled(); + + if (!isCommander || mode == deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled) { + updateBracketVisibility(false); + return; + } + + if (mode == deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) { + if (!promptCommanderSpellbookIntegration()) { + updateBracketVisibility(false); + return; + } + } + updateBracketVisibility(true); + mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled(); + if (mode != deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic) { + return; + } + + // Avoid firing if we already have a result or a request in flight + if (!bracketRefreshButton->isEnabled()) { + return; + } + + // Defer to avoid races during init / model rebuild + QTimer::singleShot(0, this, &DeckEditorDeckDockWidget::requestBracketEstimate); } ExactCard DeckEditorDeckDockWidget::getCurrentCard() @@ -740,6 +987,8 @@ void DeckEditorDeckDockWidget::retranslateUi() commentsLabel->setText(tr("&Comments:")); activeGroupCriteriaLabel->setText(tr("Group by:")); formatLabel->setText(tr("Format:")); + bracketInfoButton->setToolTip(tr("Why this bracket?")); + bracketRefreshButton->setToolTip(tr("Recalculate bracket")); hashLabel1->setText(tr("Hash:")); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 8dddf5882..ec6eed7f0 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -10,6 +10,7 @@ #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" #include "../../key_signals.h" +#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h" #include "../utility/custom_line_edit.h" #include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h" #include "deck_list_history_manager_widget.h" @@ -21,6 +22,7 @@ #include #include +class EstimateBracketResult; class DeckListModel; class AbstractTabDeckEditor; class DeckEditorDeckDockWidget : public QDockWidget @@ -33,6 +35,9 @@ public: QTreeView *deckView; QComboBox *bannerCardComboBox; void createDeckDock(); + bool promptCommanderSpellbookIntegration(); + void updateBracketVisibility(bool visible); + void requestBracketEstimate(); ExactCard getCurrentCard(); void retranslateUi(); @@ -59,6 +64,8 @@ public slots: void actSwapSelection(); void actRemoveCard(); void initializeFormats(); + void maybeAutoEstimateBracket(); + void onEstimateBracketFinished(quint64 id, QObject *requester, const EstimateBracketResult &result); signals: void selectedCardChanged(const ExactCard &card); @@ -89,6 +96,15 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; + QLabel *bracketLabel; + QLabel *bracketValueLabel; + QToolButton *bracketInfoButton; + QToolButton *bracketRefreshButton; + + BracketExplanation lastBracketExplanation; + + int requestId; + DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index e3bf209dc..f050c4be3 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -905,6 +905,56 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(), &SettingsCache::setDefaultDeckEditorType); + deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setText("?"); + deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true); + deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false); + + // Add items with userData = internal enum + deckEditorCommanderSpellbookIntegrationEnabledSelector.addItem( + tr("Disabled"), deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled); + deckEditorCommanderSpellbookIntegrationEnabledSelector.addItem( + tr("Enabled"), deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled); + deckEditorCommanderSpellbookIntegrationEnabledSelector.addItem( + tr("Automatic"), deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic); + + int storedMode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled(); + for (int i = 0; i < deckEditorCommanderSpellbookIntegrationEnabledSelector.count(); ++i) { + if (deckEditorCommanderSpellbookIntegrationEnabledSelector.itemData(i).toInt() == storedMode) { + deckEditorCommanderSpellbookIntegrationEnabledSelector.setCurrentIndex(i); + break; + } + } + + connect(&deckEditorCommanderSpellbookIntegrationEnabledSelector, + QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + int mode = deckEditorCommanderSpellbookIntegrationEnabledSelector.itemData(index).toInt(); + SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(mode); + updateCommanderSpellbookUiState(); + }); + + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("CommanderSpellbook bracket names")); // index 0 = false + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("Official Commander bracket names (approximate)")); // index 1 = true + + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex( + SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + connect(&deckEditorCommanderSpellbookIntegrationBracketNamingSelector, + QOverload::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(), [](int index) { + SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(index == 1); + }); + + updateCommanderSpellbookUiState(); + + auto *labelLayout = new QHBoxLayout; + labelLayout->setContentsMargins(0, 0, 0, 0); + labelLayout->addWidget(&deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel); + labelLayout->addWidget(&deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer); + + auto *labelWidget = new QWidget; + labelWidget->setLayout(labelLayout); + auto *deckEditorGrid = new QGridLayout; deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0); @@ -913,6 +963,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); + deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationEnabledLabel, 5, 0); + deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationEnabledSelector, 5, 1); + deckEditorGrid->addWidget(labelWidget, 6, 0); + deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationBracketNamingSelector, 6, 1); deckEditorGroupBox = new QGroupBox; deckEditorGroupBox->setLayout(deckEditorGrid); @@ -955,6 +1009,27 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) } } +void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() +{ + const int mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled(); + + const bool enabled = mode != deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled && + mode != deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted; + + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setEnabled(enabled); + deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setEnabled(enabled); + deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel.setVisible(enabled); + deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setVisible(enabled); + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setVisible(enabled); + + if (enabled) { + // Sync selector with the current stored bool + const bool useOfficial = + SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(); + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(useOfficial ? 1 : 0); + } +} + void UserInterfaceSettingsPage::retranslateUi() { generalGroupBox->setTitle(tr("General interface settings")); @@ -989,6 +1064,22 @@ void UserInterfaceSettingsPage::retranslateUi() defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); + deckEditorCommanderSpellbookIntegrationEnabledLabel.setText( + tr("CommanderSpellbook integration to estimate commander bracket")); + deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText( + deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled, tr("Disabled")); + deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText( + deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled, tr("Enabled")); + deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText( + deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic, tr("Automatic")); + deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel.setText(tr("Bracket naming")); + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setItemText( + 0, CommanderBracketNames::CommanderSpellbookBracketNames); + deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setItemText( + 1, CommanderBracketNames::OfficialCommanderBracketNames); + + deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setToolTip( + CommanderBracketNames::Explainer); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index b655a30bc..d66c056e9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -15,6 +15,7 @@ #include #include #include +#include #include inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); @@ -154,11 +155,35 @@ public: void retranslateUi() override; }; +enum deckEditorCommanderSpellbookIntegrationEnabledIndex +{ + deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled, + deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled, + deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic, + deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted, +}; + +namespace CommanderBracketNames +{ +inline const char *CommanderSpellbookBracketNames = QT_TR_NOOP("CommanderSpellbook"); +inline const char *OfficialCommanderBracketNames = QT_TR_NOOP("Official (approximate)"); +inline const char *Explainer = QT_TR_NOOP( + "The bracket system combines both objective data, as well as subjective play experience to estimate a " + "bracket for a deck.\nCommanderSpellbook's estimation is algorithmical, which means that it can only operate " + "on the objective data, not the subjective intent. \nThey have chosen to represent this by defining their " + "own bracket system which matches their algorithm.\n" + "This custom bracket system maps loosely to the standard system. \nYou may choose to use these mapped " + "standardized names if these are more familiar to you, however, you should keep in mind that these are just " + "rough estimations.\n\nAlways consider the subjective factors of the bracket system when determing a deck's " + "final bracket!"); +} // namespace CommanderBracketNames + class UserInterfaceSettingsPage : public AbstractSettingsPage { Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); + void updateCommanderSpellbookUiState(); private: QCheckBox notificationsEnabledCheckBox; @@ -182,6 +207,11 @@ private: QCheckBox visualDeckStorageSelectionAnimationCheckBox; QLabel defaultDeckEditorTypeLabel; QComboBox defaultDeckEditorTypeSelector; + QLabel deckEditorCommanderSpellbookIntegrationEnabledLabel; + QComboBox deckEditorCommanderSpellbookIntegrationEnabledSelector; + QLabel deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel; + QToolButton deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer; + QComboBox deckEditorCommanderSpellbookIntegrationBracketNamingSelector; QLabel rewindBufferingMsLabel; QSpinBox rewindBufferingMsBox; QGroupBox *generalGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp new file mode 100644 index 000000000..bebe57697 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp @@ -0,0 +1,21 @@ +#include "card_in_deck_request.h" + +void CardInDeckRequest::fromJson(const QJsonObject &json) +{ + card = json.value("card").toString(); + quantity = json.value("quantity").toInt(); +} + +QJsonObject CardInDeckRequest::toJson() const +{ + QJsonObject json; + json.insert("card", card); + json.insert("quantity", quantity); + return json; +} + +void CardInDeckRequest::debugPrint() const +{ + qDebug() << "Card:" << card; + qDebug() << "Quantity:" << quantity; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h new file mode 100644 index 000000000..5af4ce592 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h @@ -0,0 +1,23 @@ +#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H +#define COCKATRICE_CARD_IN_DECK_REQUEST_H +#include + +class CardInDeckRequest +{ +public: + // Constructor + CardInDeckRequest() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + QJsonObject toJson() const; + + // Debug method for logging + void debugPrint() const; + +private: + QString card; + int quantity; +}; + +#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h new file mode 100644 index 000000000..ba3523c08 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h @@ -0,0 +1,79 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H +#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H +#include + +namespace CommanderSpellbookBracketTag +{ +enum class BracketTag +{ + Ruthless, + Spicy, + Powerful, + Oddball, + PreconAppropriate, + Casual, + Unknown +}; + +inline static BracketTag bracketTagFromString(const QString &s) +{ + if (s == "R") + return BracketTag::Ruthless; + if (s == "S") + return BracketTag::Spicy; + if (s == "P") + return BracketTag::Powerful; + if (s == "O") + return BracketTag::Oddball; + if (s == "PA") + return BracketTag::PreconAppropriate; + if (s == "C") + return BracketTag::Casual; + return BracketTag::Unknown; +} + +inline static QString bracketTagToString(BracketTag tag) +{ + switch (tag) { + case BracketTag::Ruthless: + return "Ruthless"; + case BracketTag::Spicy: + return "Spicy"; + case BracketTag::Powerful: + return "Powerful"; + case BracketTag::Oddball: + return "Oddball"; + case BracketTag::PreconAppropriate: + return "Precon Appropriate"; + case BracketTag::Casual: + return "Casual"; + case BracketTag::Unknown: + return "Unknown"; + } + return {}; +} + +inline static QString bracketTagToOfficialString(BracketTag tag) +{ + switch (tag) { + case BracketTag::Ruthless: + return "[5] cEDH"; + case BracketTag::Spicy: + return "[4] Optimized"; + case BracketTag::Powerful: + return "[3] Upgraded"; + case BracketTag::Oddball: + return "[2] Core"; + case BracketTag::PreconAppropriate: + return "[1] Exhibition"; + case BracketTag::Casual: + return "[1] Casual"; + case BracketTag::Unknown: + return "Unknown"; + } + return {}; +} + +} // namespace CommanderSpellbookBracketTag + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp new file mode 100644 index 000000000..8837ee008 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp @@ -0,0 +1,22 @@ +#include "commander_spellbook_card_result.h" + +void CommanderSpellbookCardResult::fromJson(const QJsonObject &json) +{ + id = json.value("id").toString(); + name = json.value("name").toString(); + oracleId = json.value("oracleId").toString(); + spoiler = json.value("spoiler").toBool(); + typeLine = json.value("typeLine").toString(); + + imageUriFrontPng = json.value("imageUriFrontPng").toString(); + imageUriFrontLarge = json.value("imageUriFrontLarge").toString(); + imageUriFrontNormal = json.value("imageUriFrontNormal").toString(); + imageUriFrontSmall = json.value("imageUriFrontSmall").toString(); + imageUriFrontArtCrop = json.value("imageUriFrontArtCrop").toString(); + + imageUriBackPng = json.value("imageUriBackPng").toString(); + imageUriBackLarge = json.value("imageUriBackLarge").toString(); + imageUriBackNormal = json.value("imageUriBackNormal").toString(); + imageUriBackSmall = json.value("imageUriBackSmall").toString(); + imageUriBackArtCrop = json.value("imageUriBackArtCrop").toString(); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h new file mode 100644 index 000000000..b4ccab867 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h @@ -0,0 +1,30 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#include +#include + +class CommanderSpellbookCardResult +{ +public: + void fromJson(const QJsonObject &json); + + QString id; + QString name; + QString oracleId; + bool spoiler = false; + QString typeLine; + + QString imageUriFrontPng; + QString imageUriFrontLarge; + QString imageUriFrontNormal; + QString imageUriFrontSmall; + QString imageUriFrontArtCrop; + + QString imageUriBackPng; + QString imageUriBackLarge; + QString imageUriBackNormal; + QString imageUriBackSmall; + QString imageUriBackArtCrop; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp new file mode 100644 index 000000000..44f10016d --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp @@ -0,0 +1,118 @@ +#include "commander_spellbook_deck_request.h" + +#include +#include +#include +#include + +void CommanderSpellbookDeckRequest::fromJson(const QJsonObject &json) +{ + mainDeck.clear(); + commanderDeck.clear(); + + // Main deck + const QJsonArray mainArray = json.value("main").toArray(); + for (const QJsonValue &value : mainArray) { + if (!value.isObject()) { + continue; + } + + CardInDeckRequest card; + card.fromJson(value.toObject()); + mainDeck.append(card); + + // Max size allowed by commanderspellbook + if (mainDeck.size() >= 600) { + break; + } + } + + // Commanders + const QJsonArray commanderArray = json.value("commanders").toArray(); + for (const QJsonValue &value : commanderArray) { + if (!value.isObject()) { + continue; + } + + CardInDeckRequest card; + card.fromJson(value.toObject()); + commanderDeck.append(card); + + // Max size allowed by commanderspellbook + if (commanderDeck.size() >= 12) { + break; + } + } +} + +QJsonObject CommanderSpellbookDeckRequest::toJson() const +{ + QJsonObject json; + + QJsonArray mainArray; + for (const CardInDeckRequest &card : mainDeck) { + mainArray.append(card.toJson()); + } + + QJsonArray commanderArray; + for (const CardInDeckRequest &card : commanderDeck) { + commanderArray.append(card.toJson()); + } + + json.insert("main", mainArray); + json.insert("commanders", commanderArray); + + return json; +} + +void CommanderSpellbookDeckRequest::fromDeckList(const DeckList &deck) +{ + mainDeck.clear(); + commanderDeck.clear(); + + // --- Mainboard --- + const auto mainCards = deck.getCardNodes({DECK_ZONE_MAIN}); + for (const DecklistCardNode *node : mainCards) { + if (!node) { + continue; + } + + CardInDeckRequest req; + QJsonObject json; + json.insert("card", node->getName()); + json.insert("quantity", node->getNumber()); + req.fromJson(json); + + mainDeck.append(req); + + // Max size allowed by commanderspellbook + if (mainDeck.size() >= 600) { + break; + } + } + + // --- Commander (bannerCard) --- + const auto &metadata = deck.getMetadata(); + if (!metadata.bannerCard.name.isEmpty()) { + CardInDeckRequest commander; + QJsonObject json; + json.insert("card", metadata.bannerCard.name); + json.insert("quantity", 1); + commander.fromJson(json); + + commanderDeck.append(commander); + } +} + +void CommanderSpellbookDeckRequest::debugPrint() const +{ + qDebug() << "Main deck:"; + for (const CardInDeckRequest &card : mainDeck) { + card.debugPrint(); + } + + qDebug() << "Commanders:"; + for (const CardInDeckRequest &card : commanderDeck) { + card.debugPrint(); + } +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h new file mode 100644 index 000000000..f346874b8 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h @@ -0,0 +1,34 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#define COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#include "card_in_deck_request.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include +#include + +class CommanderSpellbookDeckRequest +{ +public: + CommanderSpellbookDeckRequest() = default; + + void fromJson(const QJsonObject &json); + QJsonObject toJson() const; + void fromDeckList(const DeckList &deck); + + void debugPrint() const; + + const QVector &main() const + { + return mainDeck; + } + const QVector &commanders() const + { + return commanderDeck; + } + +private: + QVector mainDeck; // maxItems: 600 + QVector commanderDeck; // maxItems: 12 +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp new file mode 100644 index 000000000..400e63ec9 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp @@ -0,0 +1,47 @@ +#include "commander_spellbook_estimate_bracket_result.h" + +static void parseCards(const QJsonObject &json, const QString &key, QVector &out) +{ + out.clear(); + for (const auto &v : json.value(key).toArray()) { + if (!v.isObject()) + continue; + CommanderSpellbookCardResult c; + c.fromJson(v.toObject()); + out.append(c); + } +} + +static void parseVariants(const QJsonObject &json, const QString &key, QVector &out) +{ + out.clear(); + for (const auto &v : json.value(key).toArray()) { + if (!v.isObject()) + continue; + CommanderSpellbookVariantResult vr; + vr.fromJson(v.toObject()); + out.append(vr); + } +} + +void EstimateBracketResult::fromJson(const QJsonObject &json) +{ + bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString()); + + parseCards(json, "gameChangerCards", gameChangerCards); + parseCards(json, "massLandDenialCards", massLandDenialCards); + parseCards(json, "extraTurnCards", extraTurnCards); + parseCards(json, "tutorCards", tutorCards); + + parseVariants(json, "massLandDenialTemplates", massLandDenialTemplates); + parseVariants(json, "massLandDenialCombos", massLandDenialCombos); + parseVariants(json, "extraTurnTemplates", extraTurnTemplates); + parseVariants(json, "extraTurnsCombos", extraTurnsCombos); + parseVariants(json, "tutorTemplates", tutorTemplates); + parseVariants(json, "lockCombos", lockCombos); + parseVariants(json, "skipTurnsCombos", skipTurnsCombos); + parseVariants(json, "definitelyEarlyGameTwoCardCombos", definitelyEarlyGameTwoCardCombos); + parseVariants(json, "arguablyEarlyGameTwoCardCombos", arguablyEarlyGameTwoCardCombos); + parseVariants(json, "definitelyLateGameTwoCardCombos", definitelyLateGameTwoCardCombos); + parseVariants(json, "borderlineLateGameTwoCardCombos", borderlineLateGameTwoCardCombos); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h new file mode 100644 index 000000000..701da4fc0 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h @@ -0,0 +1,33 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H +#include "commander_spellbook_card_result.h" +#include "commander_spellbook_variant_result.h" + +#include + +class EstimateBracketResult +{ +public: + void fromJson(const QJsonObject &json); + + CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown; + + QVector gameChangerCards; + QVector massLandDenialCards; + QVector extraTurnCards; + QVector tutorCards; + + QVector massLandDenialTemplates; + QVector massLandDenialCombos; + QVector extraTurnTemplates; + QVector extraTurnsCombos; + QVector tutorTemplates; + QVector lockCombos; + QVector skipTurnsCombos; + QVector definitelyEarlyGameTwoCardCombos; + QVector arguablyEarlyGameTwoCardCombos; + QVector definitelyLateGameTwoCardCombos; + QVector borderlineLateGameTwoCardCombos; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp new file mode 100644 index 000000000..0e0687605 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp @@ -0,0 +1,31 @@ +#include "commander_spellbook_variant_result.h" + +void CommanderSpellbookVariantResult::fromJson(const QJsonObject &json) +{ + id = json.value("id").toString(); + status = json.value("status").toString(); + + uses = json.value("uses").toArray(); + cardRequires = json.value("requires").toArray(); + produces = json.value("produces").toArray(); + of = json.value("of").toArray(); + includes = json.value("includes").toArray(); + + manaNeeded = json.value("manaNeeded").toArray(); + manaValueNeeded = json.value("manaValueNeeded").toArray(); + + easyPrerequisites = json.value("easyPrerequisites").toArray(); + notablePrerequisites = json.value("notablePrerequisites").toArray(); + + description = json.value("description").toString(); + notes = json.value("notes").toString(); + popularity = json.value("popularity").toDouble(); + + spoiler = json.value("spoiler").toBool(); + bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString()); + + legalities = json.value("legalities").toObject(); + prices = json.value("prices").toObject(); + + variantCount = json.value("variantCount").toInt(); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h new file mode 100644 index 000000000..76b5d1c8e --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h @@ -0,0 +1,41 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H +#include "commander_spellbook_bracket_tag.h" + +#include +#include + +class CommanderSpellbookVariantResult +{ +public: + void fromJson(const QJsonObject &json); + + QString id; + QString status; + + QJsonArray uses; + QJsonArray cardRequires; + QJsonArray produces; + QJsonArray of; + QJsonArray includes; + + QJsonArray manaNeeded; + QJsonArray manaValueNeeded; + + QJsonArray easyPrerequisites; + QJsonArray notablePrerequisites; + + QString description; + QString notes; + double popularity = 0.0; + + bool spoiler = false; + CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown; + + QJsonObject legalities; + QJsonObject prices; + + int variantCount = 0; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp new file mode 100644 index 000000000..107ed2de0 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp @@ -0,0 +1,77 @@ +#include "commander_spellbook_api_accessor.h" + +#include "api_response/commander_spellbook_deck_request.h" + +#include +#include +#include +#include +#include + +static const QUrl ESTIMATE_BRACKET_URL(QStringLiteral("https://backend.commanderspellbook.com/estimate-bracket")); + +CommanderSpellbookApiAccessor &CommanderSpellbookApiAccessor::instance() +{ + static CommanderSpellbookApiAccessor instance; + return instance; +} + +CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) : QObject(parent) +{ +} + +CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimateBracket(const DeckList &deck, + QObject *requester) +{ + CommanderSpellbookDeckRequest deckRequest; + deckRequest.fromDeckList(deck); + + QJsonDocument doc(deckRequest.toJson()); + QByteArray body = doc.toJson(QJsonDocument::Compact); + + QNetworkRequest req(ESTIMATE_BRACKET_URL); + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + + QNetworkReply *reply = network.post(req, body); + + const RequestId id = nextRequestId++; + + reply->setProperty("requestId", QVariant::fromValue(id)); + reply->setProperty("requester", QVariant::fromValue(requester)); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { onEstimateReplyFinished(reply); }); + + return id; +} + +void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + const RequestId id = reply->property("requestId").toULongLong(); + QObject *requester = reply->property("requester").value(); + + if (!requester) { + // Requester died — silently drop + return; + } + + if (reply->error() != QNetworkReply::NoError) { + emit estimateBracketError(id, requester, reply->errorString()); + return; + } + + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &err); + + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + emit estimateBracketError(id, requester, QStringLiteral("Invalid JSON response")); + return; + } + + EstimateBracketResult result; + result.fromJson(doc.object()); + + emit estimateBracketFinished(id, requester, result); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h new file mode 100644 index 000000000..682a48524 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H +#define COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H + +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +#include +#include +#include +#include + +class CommanderSpellbookApiAccessor final : public QObject +{ + Q_OBJECT + +public: + static CommanderSpellbookApiAccessor &instance(); + + using RequestId = quint64; + + RequestId estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateBracketFinished(RequestId id, QObject *requester, const EstimateBracketResult &result); + + void estimateBracketError(RequestId id, QObject *requester, const QString &errorMessage); + +private: + explicit CommanderSpellbookApiAccessor(QObject *parent = nullptr); + Q_DISABLE_COPY_MOVE(CommanderSpellbookApiAccessor) + + void onEstimateReplyFinished(QNetworkReply *reply); + + QNetworkAccessManager network; + RequestId nextRequestId = 1; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp new file mode 100644 index 000000000..d7fc406b6 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp @@ -0,0 +1,131 @@ +#include "commander_spellbook_bracket_explainer.h" + +static QString cardList(const QVector &cards, int max = 5) +{ + QStringList names; + for (int i = 0; i < cards.size() && i < max; ++i) { + names << cards[i].name; + } + + if (cards.size() > max) { + names << QString("and %1 more").arg(cards.size() - max); + } + + return names.join(", "); +} + +static QString comboCount(const QVector &variants) +{ + return QString::number(variants.size()); +} + +BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r) +{ + BracketExplanation out; + out.bracket = r.bracketTag; + + // --- Game changers --- + if (!r.gameChangerCards.isEmpty()) { + BracketExplanationSection s; + s.title = "Game-changing cards"; + s.bulletPoints << QString("Your deck contains %1 game-changing cards, such as %2.") + .arg(r.gameChangerCards.size()) + .arg(cardList(r.gameChangerCards)); + out.sections << s; + } + + // --- Tutors --- + if (!r.tutorCards.isEmpty()) { + BracketExplanationSection s; + s.title = "Tutors"; + s.bulletPoints << QString("The deck runs %1 tutor cards, including %2.") + .arg(r.tutorCards.size()) + .arg(cardList(r.tutorCards)); + out.sections << s; + } + + // --- Extra turns --- + if (!r.extraTurnCards.isEmpty()) { + BracketExplanationSection s; + s.title = "Extra turn effects"; + s.bulletPoints << QString("Extra turn spells were detected (%1), such as %2.") + .arg(r.extraTurnCards.size()) + .arg(cardList(r.extraTurnCards)); + out.sections << s; + } + + // --- Mass land denial --- + if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialCombos.isEmpty()) { + BracketExplanationSection s; + s.title = "Mass land denial"; + + if (!r.massLandDenialCards.isEmpty()) { + s.bulletPoints << QString("The deck includes %1 mass land denial cards (%2).") + .arg(r.massLandDenialCards.size()) + .arg(cardList(r.massLandDenialCards)); + } + + if (!r.massLandDenialCombos.isEmpty()) { + s.bulletPoints << QString("%1 mass land denial combo variants were identified.") + .arg(comboCount(r.massLandDenialCombos)); + } + + out.sections << s; + } + + // --- Lock / skip turns --- + if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) { + BracketExplanationSection s; + s.title = "Lock or skip-turn combos"; + + if (!r.lockCombos.isEmpty()) { + s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos)); + } + + if (!r.skipTurnsCombos.isEmpty()) { + s.bulletPoints << QString("%1 skip-turn combo variants were detected.").arg(comboCount(r.skipTurnsCombos)); + } + + out.sections << s; + } + + // --- Early-game combos --- + if (!r.definitelyEarlyGameTwoCardCombos.isEmpty() || !r.arguablyEarlyGameTwoCardCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Early-game two-card combos"; + + if (!r.definitelyEarlyGameTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 definitely early-game two-card combos were found.") + .arg(comboCount(r.definitelyEarlyGameTwoCardCombos)); + } + + if (!r.arguablyEarlyGameTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 arguably early-game two-card combos were found.") + .arg(comboCount(r.arguablyEarlyGameTwoCardCombos)); + } + + out.sections << s; + } + + // --- Late-game combos --- + if (!r.definitelyLateGameTwoCardCombos.isEmpty() || !r.borderlineLateGameTwoCardCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Late-game two-card combos"; + + if (!r.definitelyLateGameTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 definitely late-game two-card combos were found.") + .arg(comboCount(r.definitelyLateGameTwoCardCombos)); + } + + if (!r.borderlineLateGameTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 borderline late-game two-card combos were found.") + .arg(comboCount(r.borderlineLateGameTwoCardCombos)); + } + + out.sections << s; + } + + return out; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h new file mode 100644 index 000000000..53ff94dd2 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +struct BracketExplanationSection +{ + QString title; + QStringList bulletPoints; +}; + +struct BracketExplanation +{ + CommanderSpellbookBracketTag::BracketTag bracket; + QList sections; + + bool isEmpty() const + { + return sections.isEmpty(); + } +}; + +class BracketExplainer +{ +public: + static BracketExplanation explain(const EstimateBracketResult &result); +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H