From f9f5489871675dd44932ee2b6f9211b3bf006a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 14 Dec 2025 09:09:26 +0100 Subject: [PATCH 1/3] [DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander' Took 46 seconds --- cockatrice/CMakeLists.txt | 7 + .../src/client/settings/cache_settings.cpp | 30 ++ .../src/client/settings/cache_settings.h | 15 + .../deck_editor_deck_dock_widget.cpp | 263 +++++++++++++++++- .../deck_editor_deck_dock_widget.h | 16 ++ .../user_interface_settings_page.cpp | 92 ++++++ .../user_interface_settings_page.h | 30 ++ .../api_response/card_in_deck_request.cpp | 21 ++ .../api_response/card_in_deck_request.h | 23 ++ .../commander_spellbook_bracket_tag.h | 85 ++++++ .../commander_spellbook_card_result.cpp | 22 ++ .../commander_spellbook_card_result.h | 30 ++ .../commander_spellbook_deck_request.cpp | 118 ++++++++ .../commander_spellbook_deck_request.h | 34 +++ ...nder_spellbook_estimate_bracket_result.cpp | 49 ++++ ...mander_spellbook_estimate_bracket_result.h | 33 +++ .../commander_spellbook_variant_result.cpp | 31 +++ .../commander_spellbook_variant_result.h | 41 +++ .../commander_spellbook_api_accessor.cpp | 77 +++++ .../commander_spellbook_api_accessor.h | 37 +++ .../commander_spellbook_bracket_explainer.cpp | 131 +++++++++ .../commander_spellbook_bracket_explainer.h | 28 ++ 22 files changed, 1206 insertions(+), 7 deletions(-) create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index bd99d08bf..15f0e6947 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -298,6 +298,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 73e5a98a1..2e4662e2b 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -2,6 +2,8 @@ #include "../../interface/card_picture_loader/card_picture_loader_cache_method.h" #include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h" +#include "../../interface/widgets/dialogs/dlg_settings.h" +#include "../../interface/widgets/settings_page/user_interface_settings_page.h" #include "../network/update/client/release_channel.h" #include "card_counter_settings.h" #include "version_string.h" @@ -328,6 +330,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(); @@ -826,6 +836,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 8ee372766..b81cfec6e 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -158,6 +158,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); @@ -252,6 +254,8 @@ private: bool printingSelectorNavigationButtonsVisible; bool deckEditorBannerCardComboBoxVisible; bool deckEditorTagsWidgetVisible; + int deckEditorCommanderSpellbookIntegrationEnabled; + bool deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames; int visualDeckStorageSortingOrder; bool visualDeckStorageShowFolders; bool visualDeckStorageShowColorIdentity; @@ -732,6 +736,14 @@ public: { return openDeckInNewTab; } + [[nodiscard]] int getDeckEditorCommanderSpellbookIntegrationEnabled() const + { + return deckEditorCommanderSpellbookIntegrationEnabled; + } + [[nodiscard]] bool getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() const + { + return deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames; + } [[nodiscard]] int getRewindBufferingMs() const { return rewindBufferingMs; @@ -1075,6 +1087,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 f751fa225..51f996ca8 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 "../settings_page/user_interface_settings_page.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->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(); + 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->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() @@ -743,6 +990,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/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index dfa736a1a..61313bd40 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -144,6 +144,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); @@ -152,6 +202,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); deckEditorGroupBox = new QGroupBox; deckEditorGroupBox->setLayout(deckEditorGrid); @@ -194,6 +248,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")); @@ -228,6 +303,23 @@ 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/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index 6dd43ceae..7d6d701d9 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -8,13 +8,38 @@ #include #include #include +#include #include +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; @@ -38,6 +63,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..1728b8d44 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h @@ -0,0 +1,85 @@ +#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..11d10583d --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp @@ -0,0 +1,49 @@ +#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 From d2a63ca758fb40eb01620a6d5700c107b6c8d0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 16 Jun 2026 00:22:53 +0200 Subject: [PATCH 2/3] Don't hardcode brackets, fetch from cocaktrice.github.io, store as settings. Took 3 minutes Took 38 seconds Took 7 minutes --- cockatrice/CMakeLists.txt | 3 + .../src/client/settings/cache_settings.cpp | 25 +++ .../src/client/settings/cache_settings.h | 14 ++ .../deck_editor_deck_dock_widget.cpp | 26 ++-- .../deck_editor_deck_dock_widget.h | 4 +- .../commander_spellbook_bracket_tag.h | 85 ----------- ...nder_spellbook_estimate_bracket_result.cpp | 144 ++++++++++++------ ...mander_spellbook_estimate_bracket_result.h | 19 ++- .../commander_spellbook_variant_result.cpp | 2 +- .../commander_spellbook_variant_result.h | 3 +- .../commander_bracket_definitions.cpp | 49 ++++++ .../commander_bracket_definitions.h | 34 +++++ .../commander_bracket_service.cpp | 51 +++++++ .../commander_bracket_service.h | 45 ++++++ .../commander_spellbook_bracket_explainer.cpp | 87 +++++------ .../commander_spellbook_bracket_explainer.h | 2 +- .../handle_commander_brackets.cpp | 58 +++++++ .../handle_commander_brackets.h | 31 ++++ cockatrice/src/interface/window_main.cpp | 12 ++ cockatrice/src/interface/window_main.h | 1 + libcockatrice_settings/CMakeLists.txt | 2 + .../settings/commander_bracket_settings.cpp | 137 +++++++++++++++++ .../settings/commander_bracket_settings.h | 35 +++++ oracle/CMakeLists.txt | 1 + 24 files changed, 664 insertions(+), 206 deletions(-) delete mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h create mode 100644 libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp create mode 100644 libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 15f0e6947..f8db89b76 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -350,6 +350,9 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/key_signals.cpp src/interface/logger.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp + src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 2e4662e2b..5a13f7234 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -193,6 +193,7 @@ SettingsCache::SettingsCache() shortcutsSettings = new ShortcutsSettings(settingsPath, this); cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this); serversSettings = new ServersSettings(settingsPath, this); + commanderBracketSettings = new CommanderBracketSettings(settingsPath, this); messageSettings = new MessageSettings(settingsPath, this); gameFiltersSettings = new GameFiltersSettings(settingsPath, this); layoutsSettings = new LayoutsSettings(settingsPath, this); @@ -330,6 +331,7 @@ 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", @@ -338,6 +340,13 @@ SettingsCache::SettingsCache() deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames = settings->value("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names", false) .toBool(); + + auto definitions = commanderBracketSettings->loadDefinitions(); + if (definitions.isEmpty()) { + definitions = CommanderBracketSettings::defaultDefinitions(); + } + reloadBracketDefinitions(definitions); + visualDeckStorageCardSize = settings->value("interface/visualdeckstoragecardsize", 100).toInt(); visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt(); visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool(); @@ -436,6 +445,22 @@ SettingsCache::SettingsCache() clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString(); } +void SettingsCache::reloadBracketDefinitions(const QVariantList &definitions) +{ + bracketDefinitions.clear(); + for (const auto &entry : definitions) { + const auto map = entry.toMap(); + CommanderBracketDefinition def; + def.tag = map.value("tag").toString(); + def.officialName = map.value("officialName").toString(); + def.displayName = map.value("displayName").toString(); + def.explanation = map.value("explanation").toString(); + if (!def.tag.isEmpty()) { + bracketDefinitions.addDefinition(def); + } + } +} + void SettingsCache::setUseTearOffMenus(bool _useTearOffMenus) { useTearOffMenus = _useTearOffMenus; diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index b81cfec6e..b89a0b242 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -9,6 +9,7 @@ #include "../../interface/card_picture_loader/card_picture_loader_cache_method.h" #include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h" +#include "../../interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h" #include "shortcuts_settings.h" #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -203,6 +205,7 @@ private: ShortcutsSettings *shortcutsSettings; CardDatabaseSettings *cardDatabaseSettings; ServersSettings *serversSettings; + CommanderBracketSettings *commanderBracketSettings; MessageSettings *messageSettings; GameFiltersSettings *gameFiltersSettings; LayoutsSettings *layoutsSettings; @@ -212,6 +215,8 @@ private: DebugSettings *debugSettings; CardCounterSettings *cardCounterSettings; + CommanderBracketDefinitions bracketDefinitions; + QString lang; QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath, customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName, homeTabBackgroundSource; @@ -991,6 +996,15 @@ public: { return *serversSettings; } + [[nodiscard]] CommanderBracketSettings &commanderBrackets() const + { + return *commanderBracketSettings; + } + void reloadBracketDefinitions(const QVariantList &definitions); + CommanderBracketDefinitions &commanderBracketDefs() + { + return bracketDefinitions; + } [[nodiscard]] MessageSettings &messages() const { return *messageSettings; 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 51f996ca8..3969ee060 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 @@ -2,6 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../settings_page/user_interface_settings_page.h" +#include "../tabs/api/commander_spellbook/commander_bracket_service.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" @@ -168,6 +169,9 @@ void DeckEditorDeckDockWidget::createDeckDock() bracketInfoButton->setVisible(false); bracketRefreshButton->setVisible(false); + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateFinished, this, + &DeckEditorDeckDockWidget::onEstimateBracketFinished); + commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; @@ -433,29 +437,25 @@ void DeckEditorDeckDockWidget::requestBracketEstimate() bracketInfoButton->setEnabled(false); bracketValueLabel->setText(tr("Calculating…")); - requestId = CommanderSpellbookApiAccessor::instance().estimateBracket(deckStateManager->getDeckList(), this); - - connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this, - &DeckEditorDeckDockWidget::onEstimateBracketFinished); + requestId = CommanderBracketService::instance().estimateBracket(deckStateManager->getDeckList(), this); } -void DeckEditorDeckDockWidget::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, +void DeckEditorDeckDockWidget::onEstimateBracketFinished(quint64 id, QObject *requester, - const EstimateBracketResult &result) + const CommanderBracketEstimate &result) { - if (requester != this || static_cast(id) != requestId) { + if (requester != this || id != requestId) { return; } BracketExplainer explainer; - lastBracketExplanation = explainer.explain(result); + lastBracketExplanation = explainer.explain(result.rawResult); // Display bracket - if (SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames()) { - bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToOfficialString(result.bracketTag)); - } else { - bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToString(result.bracketTag)); - } + bracketValueLabel->setText( + SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() + ? result.officialName + : result.displayName); bracketRefreshButton->setEnabled(true); // Build tooltip 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 ec6eed7f0..1606d00f1 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 @@ -22,7 +22,7 @@ #include #include -class EstimateBracketResult; +struct CommanderBracketEstimate; class DeckListModel; class AbstractTabDeckEditor; class DeckEditorDeckDockWidget : public QDockWidget @@ -65,7 +65,7 @@ public slots: void actRemoveCard(); void initializeFormats(); void maybeAutoEstimateBracket(); - void onEstimateBracketFinished(quint64 id, QObject *requester, const EstimateBracketResult &result); + void onEstimateBracketFinished(quint64 id, QObject *requester, const CommanderBracketEstimate &result); signals: void selectedCardChanged(const ExactCard &card); 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 deleted file mode 100644 index 1728b8d44..000000000 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_bracket_tag.h +++ /dev/null @@ -1,85 +0,0 @@ -#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_estimate_bracket_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp index 11d10583d..16be8cb52 100644 --- 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 @@ -1,49 +1,107 @@ #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()); + bracketTag = json.value("bracketTag").toString(); - parseCards(json, "gameChangerCards", gameChangerCards); - parseCards(json, "massLandDenialCards", massLandDenialCards); - parseCards(json, "extraTurnCards", extraTurnCards); - parseCards(json, "tutorCards", tutorCards); + gameChangerCards.clear(); + massLandDenialCards.clear(); + extraTurnCards.clear(); - 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); -} + massLandDenialTemplates.clear(); + extraTurnTemplates.clear(); + + massLandDenialCombos.clear(); + extraTurnCombos.clear(); + lockCombos.clear(); + skipTurnsCombos.clear(); + + definitelyTwoCardCombos.clear(); + arguablyTwoCardCombos.clear(); + + // + // Cards + // + for (const auto &value : json.value("cards").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookCardResult card; + card.fromJson(obj.value("card").toObject()); + + if (obj.value("gameChanger").toBool()) { + gameChangerCards.append(card); + } + + if (obj.value("massLandDenial").toBool()) { + massLandDenialCards.append(card); + } + + if (obj.value("extraTurn").toBool()) { + extraTurnCards.append(card); + } + } + + // + // Templates + // + for (const auto &value : json.value("templates").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult variant; + variant.fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + massLandDenialTemplates.append(variant); + } + + if (obj.value("extraTurn").toBool()) { + extraTurnTemplates.append(variant); + } + } + + // + // Combos + // + for (const auto &value : json.value("combos").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult combo; + combo.fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + massLandDenialCombos.append(combo); + } + + if (obj.value("extraTurn").toBool()) { + extraTurnCombos.append(combo); + } + + if (obj.value("lock").toBool()) { + lockCombos.append(combo); + } + + if (obj.value("skipTurns").toBool()) { + skipTurnsCombos.append(combo); + } + + if (obj.value("definitelyTwoCard").toBool()) { + definitelyTwoCardCombos.append(combo); + } + + if (obj.value("arguablyTwoCard").toBool()) { + arguablyTwoCardCombos.append(combo); + } + } +} \ No newline at end of file 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 index 701da4fc0..8ddd3a78a 100644 --- 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 @@ -1,5 +1,6 @@ #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" @@ -10,24 +11,22 @@ class EstimateBracketResult public: void fromJson(const QJsonObject &json); - CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown; + QString bracketTag; QVector gameChangerCards; QVector massLandDenialCards; QVector extraTurnCards; - QVector tutorCards; QVector massLandDenialTemplates; - QVector massLandDenialCombos; QVector extraTurnTemplates; - QVector extraTurnsCombos; - QVector tutorTemplates; + + QVector massLandDenialCombos; + QVector extraTurnCombos; QVector lockCombos; QVector skipTurnsCombos; - QVector definitelyEarlyGameTwoCardCombos; - QVector arguablyEarlyGameTwoCardCombos; - QVector definitelyLateGameTwoCardCombos; - QVector borderlineLateGameTwoCardCombos; + + QVector definitelyTwoCardCombos; + QVector arguablyTwoCardCombos; }; -#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H +#endif \ No newline at end of file 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 index 0e0687605..05f31c535 100644 --- 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 @@ -22,7 +22,7 @@ void CommanderSpellbookVariantResult::fromJson(const QJsonObject &json) popularity = json.value("popularity").toDouble(); spoiler = json.value("spoiler").toBool(); - bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString()); + bracketTag = json.value("bracketTag").toString(); legalities = json.value("legalities").toObject(); prices = json.value("prices").toObject(); 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 index 76b5d1c8e..80893eee3 100644 --- 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 @@ -1,6 +1,5 @@ #ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H #define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H -#include "commander_spellbook_bracket_tag.h" #include #include @@ -30,7 +29,7 @@ public: double popularity = 0.0; bool spoiler = false; - CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown; + QString bracketTag; QJsonObject legalities; QJsonObject prices; diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp new file mode 100644 index 000000000..c183d1eee --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp @@ -0,0 +1,49 @@ +#include "commander_bracket_definitions.h" + +void CommanderBracketDefinitions::clear() +{ + definitions.clear(); +} + +void CommanderBracketDefinitions::addDefinition(const CommanderBracketDefinition &definition) +{ + definitions.insert(definition.tag, definition); +} + +QString CommanderBracketDefinitions::officialName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->officialName; +} + +QString CommanderBracketDefinitions::displayName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->displayName; +} + +QString CommanderBracketDefinitions::explanation(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return {}; + } + + return it->explanation; +} + +bool CommanderBracketDefinitions::contains(const QString &tag) const +{ + return definitions.contains(tag); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h new file mode 100644 index 000000000..248cffbbf --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h @@ -0,0 +1,34 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_DEFINITIONS_H +#define COCKATRICE_COMMANDER_BRACKET_DEFINITIONS_H + +#include +#include + +struct CommanderBracketDefinition +{ + QString tag; + + QString officialName; + QString displayName; + + QString explanation; +}; + +class CommanderBracketDefinitions +{ +public: + void clear(); + + void addDefinition(const CommanderBracketDefinition &definition); + + QString officialName(const QString &tag) const; + QString displayName(const QString &tag) const; + QString explanation(const QString &tag) const; + + bool contains(const QString &tag) const; + +private: + QHash definitions; +}; + +#endif // COCKATRICE_COMMANDER_BRACKET_DEFINITIONS_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp new file mode 100644 index 000000000..8c983375a --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp @@ -0,0 +1,51 @@ +#include "commander_bracket_service.h" + +#include "../../../../../client/settings/cache_settings.h" + +CommanderBracketService &CommanderBracketService::instance() +{ + static CommanderBracketService service; + return service; +} + +CommanderBracketService::CommanderBracketService(QObject *parent) : QObject(parent) +{ + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this, + &CommanderBracketService::onEstimateBracketFinished); + + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketError, this, + &CommanderBracketService::onEstimateBracketError); +} + +quint64 CommanderBracketService::estimateBracket(const DeckList &deck, QObject *requester) +{ + return CommanderSpellbookApiAccessor::instance().estimateBracket(deck, requester); +} + +void CommanderBracketService::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result) +{ + CommanderBracketEstimate estimate; + + estimate.bracketTag = result.bracketTag; + + estimate.rawResult = result; + + auto &definitions = SettingsCache::instance().commanderBracketDefs(); + + estimate.officialName = definitions.officialName(result.bracketTag); + + estimate.displayName = definitions.displayName(result.bracketTag); + + estimate.explanation = definitions.explanation(result.bracketTag); + + emit estimateFinished(id, requester, estimate); +} + +void CommanderBracketService::onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const QString &error) +{ + emit estimateError(id, requester, error); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h new file mode 100644 index 000000000..b04a8cacc --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_SERVICE_H +#define COCKATRICE_COMMANDER_BRACKET_SERVICE_H + +#include "commander_spellbook_api_accessor.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include + +struct CommanderBracketEstimate +{ + QString bracketTag; + + QString officialName; + QString displayName; + QString explanation; + + EstimateBracketResult rawResult; +}; + +class CommanderBracketService : public QObject +{ + Q_OBJECT + +public: + static CommanderBracketService &instance(); + + quint64 estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateFinished(quint64 requestId, QObject *requester, const CommanderBracketEstimate &estimate); + + void estimateError(quint64 requestId, QObject *requester, const QString &error); + +private slots: + void onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result); + + void onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, QObject *requester, const QString &error); + +private: + explicit CommanderBracketService(QObject *parent = nullptr); +}; + +#endif \ No newline at end of file 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 index d7fc406b6..f420b5d0e 100644 --- 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 @@ -24,47 +24,56 @@ 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()) { + if (!r.extraTurnCards.isEmpty() || !r.extraTurnTemplates.isEmpty() || !r.extraTurnCombos.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)); + s.title = "Extra turns"; + + if (!r.extraTurnCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 extra-turn cards (%2).") + .arg(r.extraTurnCards.size()) + .arg(cardList(r.extraTurnCards)); + } + + if (!r.extraTurnTemplates.isEmpty()) { + s.bulletPoints << QString("%1 extra-turn templates were identified.").arg(comboCount(r.extraTurnTemplates)); + } + + if (!r.extraTurnCombos.isEmpty()) { + s.bulletPoints + << QString("%1 extra-turn combo variants were identified.").arg(comboCount(r.extraTurnCombos)); + } + 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; - } + if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialTemplates.isEmpty() || !r.massLandDenialCombos.isEmpty()) { - // --- 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).") + s.bulletPoints << QString("The deck contains %1 mass land denial cards (%2).") .arg(r.massLandDenialCards.size()) .arg(cardList(r.massLandDenialCards)); } + if (!r.massLandDenialTemplates.isEmpty()) { + s.bulletPoints + << QString("%1 mass land denial templates were identified.").arg(comboCount(r.massLandDenialTemplates)); + } + if (!r.massLandDenialCombos.isEmpty()) { s.bulletPoints << QString("%1 mass land denial combo variants were identified.") .arg(comboCount(r.massLandDenialCombos)); @@ -73,10 +82,10 @@ BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r) out.sections << s; } - // --- Lock / skip turns --- if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) { + BracketExplanationSection s; - s.title = "Lock or skip-turn combos"; + s.title = "Lock pieces"; if (!r.lockCombos.isEmpty()) { s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos)); @@ -89,39 +98,19 @@ BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r) out.sections << s; } - // --- Early-game combos --- - if (!r.definitelyEarlyGameTwoCardCombos.isEmpty() || !r.arguablyEarlyGameTwoCardCombos.isEmpty()) { + if (!r.definitelyTwoCardCombos.isEmpty() || !r.arguablyTwoCardCombos.isEmpty()) { BracketExplanationSection s; - s.title = "Early-game two-card combos"; + s.title = "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.definitelyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 definite two-card combo variants were identified.") + .arg(comboCount(r.definitelyTwoCardCombos)); } - 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)); + if (!r.arguablyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 arguable two-card combo variants were identified.") + .arg(comboCount(r.arguablyTwoCardCombos)); } out.sections << s; 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 index 53ff94dd2..b4c95a343 100644 --- 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 @@ -10,7 +10,7 @@ struct BracketExplanationSection struct BracketExplanation { - CommanderSpellbookBracketTag::BracketTag bracket; + QString bracket; QList sections; bool isEmpty() const diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp new file mode 100644 index 000000000..1bb00b319 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp @@ -0,0 +1,58 @@ +#include "handle_commander_brackets.h" + +#include "../../../../../client/settings/cache_settings.h" +#include "commander_bracket_definitions.h" + +#include +#include +#include + +#define COMMANDER_BRACKET_JSON "https://cockatrice.github.io/commander-brackets.json" + +HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent) + : QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr) +{ +} + +void HandleCommanderBrackets::downloadBracketDefinitions() +{ + reply = nam->get(QNetworkRequest(QUrl(COMMANDER_BRACKET_JSON))); + + connect(reply, &QNetworkReply::finished, this, &HandleCommanderBrackets::actFinishParsingDownloadedData); +} + +void HandleCommanderBrackets::actFinishParsingDownloadedData() +{ + reply = qobject_cast(sender()); + + if (reply->error() != QNetworkReply::NoError) { + emit sigBracketDefinitionsDownloadFailed(reply->error()); + + reply->deleteLater(); + return; + } + + QJsonParseError parseError; + + auto document = QJsonDocument::fromJson(reply->readAll(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + emit sigBracketDefinitionsDownloadFailed(QNetworkReply::UnknownContentError); + + reply->deleteLater(); + return; + } + + updateBracketDefinitions(document.toVariant().toMap()); + + emit sigBracketDefinitionsDownloaded(); + + reply->deleteLater(); +} + +void HandleCommanderBrackets::updateBracketDefinitions(const QVariantMap &jsonMap) +{ + const auto bracketList = jsonMap.value("brackets").toList(); + SettingsCache::instance().commanderBrackets().saveDefinitions(bracketList); + SettingsCache::instance().reloadBracketDefinitions(bracketList); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h new file mode 100644 index 000000000..b8348b13c --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h @@ -0,0 +1,31 @@ +#ifndef COCKATRICE_HANDLE_COMMANDER_BRACKETS_H +#define COCKATRICE_HANDLE_COMMANDER_BRACKETS_H + +#include +#include +#include + +class HandleCommanderBrackets : public QObject +{ + Q_OBJECT + +public: + explicit HandleCommanderBrackets(QObject *parent = nullptr); + + void downloadBracketDefinitions(); + +signals: + void sigBracketDefinitionsDownloaded(); + void sigBracketDefinitionsDownloadFailed(QNetworkReply::NetworkError error); + +private slots: + void actFinishParsingDownloadedData(); + +private: + void updateBracketDefinitions(const QVariantMap &jsonMap); + + QNetworkAccessManager *nam; + QNetworkReply *reply; +}; + +#endif // COCKATRICE_HANDLE_COMMANDER_BRACKETS_H diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 69d3260bc..79e25b0e3 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -37,6 +37,7 @@ #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" +#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h" #include "widgets/utility/get_text_with_max.h" #include @@ -640,6 +641,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) actCheckCardUpdates(); actCheckServerUpdates(); + actCheckCommanderBracketDefinitionUpdates(); } MainWindow::~MainWindow() @@ -984,6 +986,16 @@ void MainWindow::checkClientUpdatesFinished(bool needToUpdate, bool /* isCompati } } +void MainWindow::actCheckCommanderBracketDefinitionUpdates() +{ + auto *handler = new HandleCommanderBrackets(this); + + connect(handler, &HandleCommanderBrackets::sigBracketDefinitionsDownloaded, this, + []() { qDebug() << "Bracket definitions loaded"; }); + + handler->downloadBracketDefinitions(); +} + void MainWindow::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 5f631ddc3..bbd1e2dd9 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -92,6 +92,7 @@ private slots: void cardDatabaseAllNewSetsEnabled(); void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release); + void actCheckCommanderBracketDefinitionUpdates(); void actOpenCustomFolder(); void actOpenCustomsetsFolder(); diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index 3afe6e00a..e1f24f4f5 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/settings/card_database_settings.h libcockatrice/settings/card_override_settings.h + libcockatrice/settings/commander_bracket_settings.h libcockatrice/settings/debug_settings.h libcockatrice/settings/download_settings.h libcockatrice/settings/game_filters_settings.h @@ -26,6 +27,7 @@ add_library( ${MOC_SOURCES} libcockatrice/settings/card_database_settings.cpp libcockatrice/settings/card_override_settings.cpp + libcockatrice/settings/commander_bracket_settings.cpp libcockatrice/settings/debug_settings.cpp libcockatrice/settings/download_settings.cpp libcockatrice/settings/game_filters_settings.cpp diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp new file mode 100644 index 000000000..d9b39bbd7 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp @@ -0,0 +1,137 @@ +#include "commander_bracket_settings.h" + +#include + +QVariantList CommanderBracketSettings::defaultDefinitions() +{ + return { + QVariantMap{{"tag", "R"}, + {"officialName", "[5] cEDH"}, + {"displayName", "Ruthless"}, + {"explanation", + "Top-tier competitive decks with maximum optimization, fast combos, and minimal variance."}}, + QVariantMap{{"tag", "S"}, + {"officialName", "[4] Optimized"}, + {"displayName", "Spicy"}, + {"explanation", "Highly tuned decks with strong synergy and occasional combo finishes."}}, + QVariantMap{{"tag", "P"}, + {"officialName", "[3] Upgraded"}, + {"displayName", "Powerful"}, + {"explanation", "Focused decks with clear win conditions and solid consistency."}}, + QVariantMap{{"tag", "O"}, + {"officialName", "[2] Core"}, + {"displayName", "Oddball"}, + {"explanation", "Unconventional or thematic decks with some structure but non-standard choices."}}, + QVariantMap{{"tag", "PA"}, + {"officialName", "[1] Exhibition"}, + {"displayName", "Precon Appropriate"}, + {"explanation", "Lightly upgraded preconstructed decks or very casual builds."}}, + QVariantMap{{"tag", "C"}, + {"officialName", "[1] Casual"}, + {"displayName", "Casual"}, + {"explanation", "Relaxed decks with no strict optimization goals."}}, + QVariantMap{{"tag", "U"}, + {"officialName", "Unknown"}, + {"displayName", "Unknown"}, + {"explanation", "Unclassified or missing bracket definition."}}, + }; +} + +CommanderBracketSettings::CommanderBracketSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "commander_brackets.ini", "commander_brackets", QString(), parent) +{ +} + +void CommanderBracketSettings::setSchemaVersion(int version) +{ + setValue(version, "schemaVersion"); +} + +int CommanderBracketSettings::getSchemaVersion() const +{ + QVariant value = getValue("schemaVersion"); + return value.isValid() ? value.toInt() : 0; +} + +void CommanderBracketSettings::clearDefinitions() +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + settings.remove(""); + settings.endGroup(); + + settings.sync(); +} + +void CommanderBracketSettings::saveDefinitions(const QVariantList &definitions) +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + settings.remove(""); + + settings.setValue("schemaVersion", CurrentSchemaVersion); + + for (const auto &entry : definitions) { + QVariantMap map = entry.toMap(); + + QString tag = map.value("tag").toString(); + + if (tag.isEmpty()) { + continue; + } + + settings.beginGroup(tag); + + settings.setValue("officialName", map.value("officialName")); + + settings.setValue("displayName", map.value("displayName")); + + settings.setValue("explanation", map.value("explanation")); + + settings.endGroup(); + } + + settings.endGroup(); + + settings.sync(); +} + +QVariantList CommanderBracketSettings::loadDefinitions() const +{ + QVariantList result; + + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + int version = settings.value("schemaVersion", 0).toInt(); + + if (version != CurrentSchemaVersion) { + settings.endGroup(); + return result; + } + + QStringList groups = settings.childGroups(); + + for (const QString &tag : groups) { + settings.beginGroup(tag); + + QVariantMap map; + + map["tag"] = tag; + map["officialName"] = settings.value("officialName"); + map["displayName"] = settings.value("displayName"); + map["explanation"] = settings.value("explanation"); + + result.append(map); + + settings.endGroup(); + } + + settings.endGroup(); + + return result; +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h new file mode 100644 index 000000000..8ce476d34 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h @@ -0,0 +1,35 @@ +#ifndef COMMANDER_BRACKET_SETTINGS_H +#define COMMANDER_BRACKET_SETTINGS_H + +#include "settings_manager.h" + +#include +#include + +class CommanderBracketSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + static constexpr int CurrentSchemaVersion = 1; + + static QVariantList defaultDefinitions(); + + void clearDefinitions(); + + void saveDefinitions(const QVariantList &definitions); + + QVariantList loadDefinitions() const; + + void setSchemaVersion(int version); + int getSchemaVersion() const; + +private: + explicit CommanderBracketSettings(const QString &settingPath, QObject *parent = nullptr); + + CommanderBracketSettings(const CommanderBracketSettings &) = delete; + CommanderBracketSettings &operator=(const CommanderBracketSettings &) = delete; +}; + +#endif // COMMANDER_BRACKET_SETTINGS_H \ No newline at end of file diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index a51982625..5b4a31938 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -32,6 +32,7 @@ set(oracle_SOURCES ../cockatrice/src/interface/theme_manager.cpp ../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp ../cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp + ../cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp ${VERSION_STRING_CPP} ) From 32258bdfd7993d5b1e03f2b3e34688226000167a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 18 Jun 2026 10:40:34 +0200 Subject: [PATCH 3/3] Change requestId variable type to quint64 from int. Took 6 minutes --- .../widgets/deck_editor/deck_editor_deck_dock_widget.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1606d00f1..05a73b828 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 @@ -103,7 +103,7 @@ private: BracketExplanation lastBracketExplanation; - int requestId; + quint64 requestId; DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;