This commit is contained in:
BruebachL 2026-03-18 02:15:49 -07:00 committed by GitHub
commit f49c4f6b50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1195 additions and 6 deletions

View File

@ -276,6 +276,13 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp

View File

@ -1,5 +1,6 @@
#include "cache_settings.h"
#include "../../interface/widgets/dialogs/dlg_settings.h"
#include "../network/update/client/release_channel.h"
#include "card_counter_settings.h"
#include "version_string.h"
@ -302,6 +303,14 @@ SettingsCache::SettingsCache()
deckEditorBannerCardComboBoxVisible =
settings->value("interface/deckeditorbannercardcomboboxvisible", true).toBool();
deckEditorTagsWidgetVisible = settings->value("interface/deckeditortagswidgetvisible", true).toBool();
deckEditorCommanderSpellbookIntegrationEnabled =
settings
->value("interface/deck_editor/commander_spellbook_integration/enabled",
deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted)
.toInt();
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames =
settings->value("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names", false)
.toBool();
visualDeckStorageCardSize = settings->value("interface/visualdeckstoragecardsize", 100).toInt();
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
@ -798,6 +807,26 @@ void SettingsCache::setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEdito
emit deckEditorTagsWidgetVisibleChanged(deckEditorTagsWidgetVisible);
}
void SettingsCache::setDeckEditorCommanderSpellbookIntegrationEnabled(
int _deckEditorCommanderSpellbookIntegrationEnabled)
{
deckEditorCommanderSpellbookIntegrationEnabled = _deckEditorCommanderSpellbookIntegrationEnabled;
settings->setValue("interface/deck_editor/commander_spellbook_integration/enabled",
deckEditorCommanderSpellbookIntegrationEnabled);
emit deckEditorCommanderSpellbookIntegrationEnabledChanged(deckEditorCommanderSpellbookIntegrationEnabled);
}
void SettingsCache::setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(
bool _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames)
{
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames =
_deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames;
settings->setValue("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names",
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames);
emit deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged(
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames);
}
void SettingsCache::setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder)
{
visualDeckStorageSortingOrder = _visualDeckStorageSortingOrder;

View File

@ -156,6 +156,8 @@ signals:
void printingSelectorNavigationButtonsVisibleChanged();
void deckEditorBannerCardComboBoxVisibleChanged(bool _visible);
void deckEditorTagsWidgetVisibleChanged(bool _visible);
void deckEditorCommanderSpellbookIntegrationEnabledChanged(int _enabled);
void deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool _useOfficialBracketNames);
void visualDeckStorageShowTagFilterChanged(bool _visible);
void visualDeckStorageDefaultTagsListChanged();
void visualDeckStorageShowColorIdentityChanged(bool _visible);
@ -246,6 +248,8 @@ private:
bool printingSelectorNavigationButtonsVisible;
bool deckEditorBannerCardComboBoxVisible;
bool deckEditorTagsWidgetVisible;
int deckEditorCommanderSpellbookIntegrationEnabled;
bool deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames;
int visualDeckStorageSortingOrder;
bool visualDeckStorageShowFolders;
bool visualDeckStorageShowColorIdentity;
@ -719,6 +723,14 @@ public:
{
return openDeckInNewTab;
}
[[nodiscard]] int getDeckEditorCommanderSpellbookIntegrationEnabled() const
{
return deckEditorCommanderSpellbookIntegrationEnabled;
}
[[nodiscard]] bool getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() const
{
return deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames;
}
[[nodiscard]] int getRewindBufferingMs() const
{
return rewindBufferingMs;
@ -1050,6 +1062,9 @@ public slots:
void setPrintingSelectorNavigationButtonsVisible(QT_STATE_CHANGED_T _navigationButtonsVisible);
void setDeckEditorBannerCardComboBoxVisible(QT_STATE_CHANGED_T _deckEditorBannerCardComboBoxVisible);
void setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEditorTagsWidgetVisible);
void setDeckEditorCommanderSpellbookIntegrationEnabled(int _deckEditorCommanderSpellbookIntegrationEnabled);
void setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(
bool _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames);
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);

View File

@ -1,13 +1,19 @@
#include "deck_editor_deck_dock_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../dialogs/dlg_settings.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_api_accessor.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h"
#include "deck_list_style_proxy.h"
#include "deck_state_manager.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDockWidget>
#include <QFormLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMessageBox>
#include <QSplitter>
#include <QTextEdit>
#include <libcockatrice/card/database/card_database_manager.h>
@ -131,6 +137,37 @@ void DeckEditorDeckDockWidget::createDeckDock()
formatComboBox->addItem(tr("Loading Database..."));
formatComboBox->setEnabled(false); // Disable until loaded
// --- Commander bracket row (hidden, unless format is 'commander') ---
bracketLabel = new QLabel(tr("Bracket:"), this);
bracketValueLabel = new QLabel(this);
bracketValueLabel->setText("-");
bracketValueLabel->setObjectName("bracketValueLabel");
bracketInfoButton = new QToolButton(this);
bracketInfoButton->setText("?");
bracketInfoButton->setAutoRaise(true);
bracketInfoButton->setEnabled(false);
bracketRefreshButton = new QToolButton(this);
bracketRefreshButton->setIcon(QPixmap("theme:icons/reload"));
bracketRefreshButton->setAutoRaise(true);
connect(bracketRefreshButton, &QToolButton::clicked, this, &DeckEditorDeckDockWidget::requestBracketEstimate);
if (SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled() !=
deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) {
connect(&SettingsCache::instance(), &SettingsCache::deckEditorCommanderSpellbookIntegrationEnabledChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
connect(&SettingsCache::instance(),
&SettingsCache::deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
}
bracketLabel->setVisible(false);
bracketValueLabel->setVisible(false);
bracketInfoButton->setVisible(false);
bracketRefreshButton->setVisible(false);
commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit;
@ -216,13 +253,23 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(formatLabel, 2, 0);
upperLayout->addWidget(formatComboBox, 2, 1);
upperLayout->addWidget(bannerCardLabel, 3, 0);
upperLayout->addWidget(bannerCardComboBox, 3, 1);
upperLayout->addWidget(bracketLabel, 3, 0);
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1);
auto *bracketRow = new QHBoxLayout;
bracketRow->addWidget(bracketValueLabel);
bracketRow->addWidget(bracketInfoButton);
bracketRow->addWidget(bracketRefreshButton);
bracketRow->addStretch();
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
upperLayout->addLayout(bracketRow, 3, 1);
upperLayout->addWidget(bannerCardLabel, 4, 0);
upperLayout->addWidget(bannerCardComboBox, 4, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1");
@ -280,6 +327,151 @@ void DeckEditorDeckDockWidget::createDeckDock()
}
}
bool DeckEditorDeckDockWidget::promptCommanderSpellbookIntegration()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("CommanderSpellbook integration"));
auto *mainLayout = new QVBoxLayout(&dialog);
// Main text
auto *label = new QLabel(tr("CommanderSpellbook can analyze your deck and estimate its Commander bracket.\n\n"
"This sends your deck list to an external service.\n\n"
"CommanderSpellbook uses its own bracket naming system based on their own algorithm. "
"These names can be mapped to the official Commander brackets, but the mapping "
"is only an approximation."));
label->setWordWrap(true);
mainLayout->addWidget(label);
// Naming selector
auto *formLayout = new QFormLayout;
auto *namingCombo = new QComboBox(&dialog);
namingCombo->addItem(tr("CommanderSpellbook bracket names"));
namingCombo->addItem(tr("Official Commander bracket names (approximate)"));
namingCombo->setCurrentIndex(
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0);
// Create label + explainer button
auto *labelWidget = new QWidget(&dialog);
auto *labelLayout = new QHBoxLayout(labelWidget);
labelLayout->setContentsMargins(0, 0, 0, 0);
auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget);
auto *explainerButton = new QToolButton(labelWidget);
explainerButton->setText("?");
explainerButton->setAutoRaise(true);
explainerButton->setEnabled(false);
explainerButton->setToolTip(CommanderBracketNames::Explainer);
labelLayout->addWidget(namingLabel);
labelLayout->addWidget(explainerButton);
labelLayout->addStretch(); // push the button next to label, combo stays aligned
// Add row with the custom label widget
formLayout->addRow(labelWidget, namingCombo);
mainLayout->addLayout(formLayout);
// Buttons
auto *buttonBox = new QDialogButtonBox(&dialog);
auto *enableBtn = buttonBox->addButton(tr("Enable"), QDialogButtonBox::AcceptRole);
auto *automaticBtn = buttonBox->addButton(tr("Automatic"), QDialogButtonBox::ApplyRole);
auto *disableBtn = buttonBox->addButton(tr("Disable"), QDialogButtonBox::RejectRole);
mainLayout->addWidget(buttonBox);
// Track which button was clicked
QAbstractButton *clickedButton = nullptr;
QObject::connect(buttonBox, &QDialogButtonBox::clicked, &dialog, [&](QAbstractButton *btn) {
clickedButton = btn;
dialog.accept();
});
dialog.exec();
// Persist naming choice (if not disabled)
if (clickedButton != disableBtn) {
bool useOfficial = namingCombo->currentIndex() == 1;
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial);
}
connect(&SettingsCache::instance(), &SettingsCache::deckEditorCommanderSpellbookIntegrationEnabledChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
connect(&SettingsCache::instance(),
&SettingsCache::deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
// Persist integration mode
if (clickedButton == disableBtn) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled);
return false;
}
if (clickedButton == enableBtn) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(
deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled);
return true;
}
if (clickedButton == automaticBtn) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(
deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic);
return true;
}
return false;
}
void DeckEditorDeckDockWidget::updateBracketVisibility(bool visible)
{
bracketLabel->setVisible(visible);
bracketValueLabel->setVisible(visible);
bracketInfoButton->setVisible(visible);
bracketRefreshButton->setVisible(visible);
}
void DeckEditorDeckDockWidget::requestBracketEstimate()
{
bracketRefreshButton->setEnabled(false);
bracketInfoButton->setEnabled(false);
bracketValueLabel->setText(tr("Calculating…"));
requestId = CommanderSpellbookApiAccessor::instance().estimateBracket(*deckStateManager->getModel()->getDeckList(), this);
connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this,
&DeckEditorDeckDockWidget::onEstimateBracketFinished);
}
void DeckEditorDeckDockWidget::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id,
QObject *requester,
const EstimateBracketResult &result)
{
if (requester != this || static_cast<int>(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 &section : lastBracketExplanation.sections) {
tooltip += "<b>" + section.title + "</b><br>";
for (const auto &line : section.bulletPoints) {
tooltip += "" + line + "<br>";
}
tooltip += "<br>";
}
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<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
QString formatKey;
if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString();
deckStateManager->setFormat(formatKey);
} else {
deckStateManager->setFormat(""); // clear format if deselected
}
const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0);
const bool commanderSpellbookIntegrationEnabled =
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled() !=
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled;
const bool bracketVisible = isCommander && commanderSpellbookIntegrationEnabled;
updateBracketVisibility(bracketVisible);
if (!isCommander) {
bracketValueLabel->setText("-");
bracketInfoButton->setToolTip({});
bracketInfoButton->setEnabled(false);
bracketRefreshButton->setEnabled(false);
} else {
bracketRefreshButton->setEnabled(true);
maybeAutoEstimateBracket();
}
});
maybeAutoEstimateBracket();
}
void DeckEditorDeckDockWidget::maybeAutoEstimateBracket()
{
const QString formatKey = deckStateManager->getModel()->getDeckList()->getGameFormat();
const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0);
int mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
if (!isCommander || mode == deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled) {
updateBracketVisibility(false);
return;
}
if (mode == deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) {
if (!promptCommanderSpellbookIntegration()) {
updateBracketVisibility(false);
return;
}
}
updateBracketVisibility(true);
mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
if (mode != deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic) {
return;
}
// Avoid firing if we already have a result or a request in flight
if (!bracketRefreshButton->isEnabled()) {
return;
}
// Defer to avoid races during init / model rebuild
QTimer::singleShot(0, this, &DeckEditorDeckDockWidget::requestBracketEstimate);
}
ExactCard DeckEditorDeckDockWidget::getCurrentCard()
@ -740,6 +987,8 @@ void DeckEditorDeckDockWidget::retranslateUi()
commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:"));
formatLabel->setText(tr("Format:"));
bracketInfoButton->setToolTip(tr("Why this bracket?"));
bracketRefreshButton->setToolTip(tr("Recalculate bracket"));
hashLabel1->setText(tr("Hash:"));

View File

@ -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 <QTreeView>
#include <libcockatrice/card/card_info.h>
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);

View File

@ -905,6 +905,56 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&defaultDeckEditorTypeSelector, QOverload<int>::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<int>::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<int>::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(), [](int index) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(index == 1);
});
updateCommanderSpellbookUiState();
auto *labelLayout = new QHBoxLayout;
labelLayout->setContentsMargins(0, 0, 0, 0);
labelLayout->addWidget(&deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel);
labelLayout->addWidget(&deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer);
auto *labelWidget = new QWidget;
labelWidget->setLayout(labelLayout);
auto *deckEditorGrid = new QGridLayout;
deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0);
deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0);
@ -913,6 +963,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationEnabledLabel, 5, 0);
deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationEnabledSelector, 5, 1);
deckEditorGrid->addWidget(labelWidget, 6, 0);
deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationBracketNamingSelector, 6, 1);
deckEditorGroupBox = new QGroupBox;
deckEditorGroupBox->setLayout(deckEditorGrid);
@ -955,6 +1009,27 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i)
}
}
void UserInterfaceSettingsPage::updateCommanderSpellbookUiState()
{
const int mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
const bool enabled = mode != deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled &&
mode != deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted;
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setEnabled(enabled);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setEnabled(enabled);
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel.setVisible(enabled);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setVisible(enabled);
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setVisible(enabled);
if (enabled) {
// Sync selector with the current stored bool
const bool useOfficial =
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames();
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(useOfficial ? 1 : 0);
}
}
void UserInterfaceSettingsPage::retranslateUi()
{
generalGroupBox->setTitle(tr("General interface settings"));
@ -989,6 +1064,22 @@ void UserInterfaceSettingsPage::retranslateUi()
defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor"));
deckEditorCommanderSpellbookIntegrationEnabledLabel.setText(
tr("CommanderSpellbook integration to estimate commander bracket"));
deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText(
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled, tr("Disabled"));
deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText(
deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled, tr("Enabled"));
deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText(
deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic, tr("Automatic"));
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel.setText(tr("Bracket naming"));
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setItemText(
0, CommanderBracketNames::CommanderSpellbookBracketNames);
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setItemText(
1, CommanderBracketNames::OfficialCommanderBracketNames);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setToolTip(
CommanderBracketNames::Explainer);
replayGroupBox->setTitle(tr("Replay settings"));
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
rewindBufferingMsBox.setSuffix(" ms");

View File

@ -15,6 +15,7 @@
#include <QLoggingCategory>
#include <QPushButton>
#include <QSpinBox>
#include <QToolButton>
#include <libcockatrice/utility/macros.h>
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
@ -154,11 +155,35 @@ public:
void retranslateUi() override;
};
enum deckEditorCommanderSpellbookIntegrationEnabledIndex
{
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled,
deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled,
deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic,
deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted,
};
namespace CommanderBracketNames
{
inline const char *CommanderSpellbookBracketNames = QT_TR_NOOP("CommanderSpellbook");
inline const char *OfficialCommanderBracketNames = QT_TR_NOOP("Official (approximate)");
inline const char *Explainer = QT_TR_NOOP(
"The bracket system combines both objective data, as well as subjective play experience to estimate a "
"bracket for a deck.\nCommanderSpellbook's estimation is algorithmical, which means that it can only operate "
"on the objective data, not the subjective intent. \nThey have chosen to represent this by defining their "
"own bracket system which matches their algorithm.\n"
"This custom bracket system maps loosely to the standard system. \nYou may choose to use these mapped "
"standardized names if these are more familiar to you, however, you should keep in mind that these are just "
"rough estimations.\n\nAlways consider the subjective factors of the bracket system when determing a deck's "
"final bracket!");
} // namespace CommanderBracketNames
class UserInterfaceSettingsPage : public AbstractSettingsPage
{
Q_OBJECT
private slots:
void setNotificationEnabled(QT_STATE_CHANGED_T);
void updateCommanderSpellbookUiState();
private:
QCheckBox notificationsEnabledCheckBox;
@ -182,6 +207,11 @@ private:
QCheckBox visualDeckStorageSelectionAnimationCheckBox;
QLabel defaultDeckEditorTypeLabel;
QComboBox defaultDeckEditorTypeSelector;
QLabel deckEditorCommanderSpellbookIntegrationEnabledLabel;
QComboBox deckEditorCommanderSpellbookIntegrationEnabledSelector;
QLabel deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel;
QToolButton deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer;
QComboBox deckEditorCommanderSpellbookIntegrationBracketNamingSelector;
QLabel rewindBufferingMsLabel;
QSpinBox rewindBufferingMsBox;
QGroupBox *generalGroupBox;

View File

@ -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;
}

View File

@ -0,0 +1,23 @@
#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H
#define COCKATRICE_CARD_IN_DECK_REQUEST_H
#include <QJsonObject>
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

View File

@ -0,0 +1,79 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H
#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H
#include <QString>
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

View File

@ -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();
}

View File

@ -0,0 +1,30 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#include <QJsonObject>
#include <QString>
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

View File

@ -0,0 +1,118 @@
#include "commander_spellbook_deck_request.h"
#include <QDebug>
#include <QJsonArray>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
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();
}
}

View File

@ -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 <QJsonObject>
#include <QVector>
class CommanderSpellbookDeckRequest
{
public:
CommanderSpellbookDeckRequest() = default;
void fromJson(const QJsonObject &json);
QJsonObject toJson() const;
void fromDeckList(const DeckList &deck);
void debugPrint() const;
const QVector<CardInDeckRequest> &main() const
{
return mainDeck;
}
const QVector<CardInDeckRequest> &commanders() const
{
return commanderDeck;
}
private:
QVector<CardInDeckRequest> mainDeck; // maxItems: 600
QVector<CardInDeckRequest> commanderDeck; // maxItems: 12
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H

View File

@ -0,0 +1,47 @@
#include "commander_spellbook_estimate_bracket_result.h"
static void parseCards(const QJsonObject &json, const QString &key, QVector<CommanderSpellbookCardResult> &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<CommanderSpellbookVariantResult> &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);
}

View File

@ -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 <QVector>
class EstimateBracketResult
{
public:
void fromJson(const QJsonObject &json);
CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown;
QVector<CommanderSpellbookCardResult> gameChangerCards;
QVector<CommanderSpellbookCardResult> massLandDenialCards;
QVector<CommanderSpellbookCardResult> extraTurnCards;
QVector<CommanderSpellbookCardResult> tutorCards;
QVector<CommanderSpellbookVariantResult> massLandDenialTemplates;
QVector<CommanderSpellbookVariantResult> massLandDenialCombos;
QVector<CommanderSpellbookVariantResult> extraTurnTemplates;
QVector<CommanderSpellbookVariantResult> extraTurnsCombos;
QVector<CommanderSpellbookVariantResult> tutorTemplates;
QVector<CommanderSpellbookVariantResult> lockCombos;
QVector<CommanderSpellbookVariantResult> skipTurnsCombos;
QVector<CommanderSpellbookVariantResult> definitelyEarlyGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> arguablyEarlyGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> definitelyLateGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> borderlineLateGameTwoCardCombos;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H

View File

@ -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();
}

View File

@ -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 <QJsonArray>
#include <QJsonObject>
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

View File

@ -0,0 +1,77 @@
#include "commander_spellbook_api_accessor.h"
#include "api_response/commander_spellbook_deck_request.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkRequest>
#include <QUrl>
#include <version_string.h>
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<QObject *>();
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);
}

View File

@ -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 <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <libcockatrice/deck_list/deck_list.h>
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

View File

@ -0,0 +1,131 @@
#include "commander_spellbook_bracket_explainer.h"
static QString cardList(const QVector<CommanderSpellbookCardResult> &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<CommanderSpellbookVariantResult> &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;
}

View File

@ -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<BracketExplanationSection> sections;
bool isEmpty() const
{
return sections.isEmpty();
}
};
class BracketExplainer
{
public:
static BracketExplanation explain(const EstimateBracketResult &result);
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H