diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 24c6263fe..60e3ce08f 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -240,8 +240,12 @@ set(cockatrice_SOURCES src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp + src/interface/widgets/playmat/playmat_collection_dialog.cpp + src/interface/widgets/playmat/playmat_collection_dialog.h src/interface/widgets/playmat/playmat_preview_widget.cpp src/interface/widgets/playmat/playmat_settings_dialog.cpp + src/interface/widgets/playmat/playmat_settings_utils.cpp + src/interface/widgets/playmat/playmat_settings_utils.h src/interface/widgets/quick_settings/settings_button_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp src/interface/widgets/replay/replay_manager.cpp diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 69941040f..c8c9fd357 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -8,6 +8,7 @@ #include "../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h" #include "../../interface/widgets/dialogs/dlg_load_remote_deck.h" +#include "../../interface/widgets/playmat/playmat_settings_utils.h" #include "../../interface/widgets/tabs/tab_game.h" #include "deck_view.h" @@ -277,7 +278,11 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath) void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck) { - QString deckString = deck.writeToString_Native(); + // Bake the user-level playmat resolution (override > deck > fallback list) + // into the uploaded deck so the server, local and remote players, and + // replays all derive the same effective playmat. + const DeckList effectiveDeck = applyUserPlaymatSettings(deck, SettingsCache::instance().userInterface()); + QString deckString = effectiveDeck.writeToString_Native(); if (deckString.length() > MAX_FILE_LENGTH) { QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size.")); 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 a398e9086..a06407a34 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 @@ -3,6 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../playmat/playmat_settings_dialog.h" +#include "../playmat/playmat_settings_utils.h" #include "../settings_page/user_interface_settings_page.h" #include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "deck_list_style_proxy.h" @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -468,6 +470,14 @@ void DeckEditorDeckDockWidget::openPlaymatSettings() deckStateManager->setPlaymatCard(newCard); deckStateManager->setPlaymatParams(newParams); } + + // "Also set as my default playmat": make this the fixed user-level + // fallback, so decks without a playmat of their own use it. + if (dialog.useAsDefault() && !newCard.isEmpty()) { + auto &interfaceSettings = SettingsCache::instance().userInterface(); + interfaceSettings.setPlaymatFallbackList({storedFromResolution({newCard, newParams})}); + interfaceSettings.setPlaymatFallbackMode(0); // PlaymatFallbackMode::Fixed + } updatePlaymatLabel(); } } diff --git a/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.cpp new file mode 100644 index 000000000..7ad1308ad --- /dev/null +++ b/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.cpp @@ -0,0 +1,193 @@ +#include "playmat_collection_dialog.h" + +#include "../../../client/settings/cache_settings.h" +#include "playmat_settings_dialog.h" +#include "playmat_settings_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +PlaymatCollectionDialog::PlaymatCollectionDialog(QWidget *parent) : QDialog(parent) +{ + setMinimumWidth(420); + setupUi(); + retranslateUi(); +} + +void PlaymatCollectionDialog::accept() +{ + auto &interfaceSettings = SettingsCache::instance().userInterface(); + interfaceSettings.setPlaymatFallbackList(playmats); + interfaceSettings.setPlaymatFallbackMode(modeCombo->currentData().toInt()); + QDialog::accept(); +} + +int PlaymatCollectionDialog::currentRow() const +{ + return playmatList->currentRow(); +} + +void PlaymatCollectionDialog::setupUi() +{ + auto &interfaceSettings = SettingsCache::instance().userInterface(); + playmats = interfaceSettings.getPlaymatFallbackList(); + + playmatList = new QListWidget; + for (const StoredPlaymat &entry : playmats) { + playmatList->addItem(entry.name); + } + connect(playmatList, &QListWidget::itemSelectionChanged, this, &PlaymatCollectionDialog::selectionChanged); + connect(playmatList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { editPlaymat(); }); + + addButton = new QPushButton; + editButton = new QPushButton; + removeButton = new QPushButton; + moveUpButton = new QPushButton; + moveDownButton = new QPushButton; + + connect(addButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::addPlaymat); + connect(editButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::editPlaymat); + connect(removeButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::removePlaymat); + connect(moveUpButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatUp); + connect(moveDownButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatDown); + + auto *listButtons = new QVBoxLayout; + listButtons->addWidget(addButton); + listButtons->addWidget(editButton); + listButtons->addWidget(removeButton); + listButtons->addWidget(moveUpButton); + listButtons->addWidget(moveDownButton); + listButtons->addStretch(); + + auto *listRow = new QHBoxLayout; + listRow->addWidget(playmatList, 1); + listRow->addLayout(listButtons); + + modeCombo = new QComboBox; + modeCombo->addItem(QString(), 0); + modeCombo->addItem(QString(), 1); + modeCombo->addItem(QString(), 2); + const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackMode()); + if (modeIndex >= 0) { + modeCombo->setCurrentIndex(modeIndex); + } + + auto *modeRow = new QHBoxLayout; + modeLabel = new QLabel; + modeLabel->setBuddy(modeCombo); + modeRow->addWidget(modeLabel); + modeRow->addWidget(modeCombo, 1); + + auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttonBox, &QDialogButtonBox::accepted, this, &PlaymatCollectionDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + auto *root = new QVBoxLayout; + root->addLayout(listRow); + root->addLayout(modeRow); + root->addWidget(buttonBox); + setLayout(root); + + selectionChanged(); +} + +void PlaymatCollectionDialog::selectionChanged() +{ + const bool hasSelection = playmatList->currentRow() >= 0; + editButton->setEnabled(hasSelection); + removeButton->setEnabled(hasSelection); + moveUpButton->setEnabled(hasSelection && playmatList->currentRow() > 0); + moveDownButton->setEnabled(hasSelection && playmatList->currentRow() < playmatList->count() - 1); +} + +void PlaymatCollectionDialog::appendEntry(const StoredPlaymat &entry) +{ + playmats.append(entry); + playmatList->addItem(entry.name); + playmatList->setCurrentRow(playmatList->count() - 1); +} + +void PlaymatCollectionDialog::addPlaymat() +{ + PlaymatSettingsDialog dialog(CardRef{}, PlaymatParams{}, this); + if (dialog.exec() == QDialog::Accepted) { + const CardRef card = dialog.card(); + if (!card.isEmpty()) { + appendEntry(storedFromResolution({card, dialog.params()})); + } + } +} + +void PlaymatCollectionDialog::editPlaymat() +{ + const int row = currentRow(); + if (row < 0) { + return; + } + + const PlaymatResolution current = resolutionFromStoredPlaymat(playmats.at(row)); + PlaymatSettingsDialog dialog(current.card, current.params, this); + if (dialog.exec() == QDialog::Accepted) { + const CardRef card = dialog.card(); + if (card.isEmpty()) { + return; // Removal is handled by the Remove button + } + playmats[row] = storedFromResolution({card, dialog.params()}); + playmatList->item(row)->setText(card.name); + } +} + +void PlaymatCollectionDialog::removePlaymat() +{ + const int row = currentRow(); + if (row < 0) { + return; + } + playmats.removeAt(row); + delete playmatList->takeItem(row); + selectionChanged(); +} + +void PlaymatCollectionDialog::movePlaymatUp() +{ + const int row = currentRow(); + if (row <= 0) { + return; + } + playmats.swapItemsAt(row, row - 1); + playmatList->insertItem(row - 1, playmatList->takeItem(row)); + playmatList->setCurrentRow(row - 1); + selectionChanged(); +} + +void PlaymatCollectionDialog::movePlaymatDown() +{ + const int row = currentRow(); + if (row < 0 || row >= playmats.size() - 1) { + return; + } + playmats.swapItemsAt(row, row + 1); + playmatList->insertItem(row + 1, playmatList->takeItem(row)); + playmatList->setCurrentRow(row + 1); + selectionChanged(); +} + +void PlaymatCollectionDialog::retranslateUi() +{ + setWindowTitle(tr("Default Playmats")); + addButton->setText(tr("Add...")); + editButton->setText(tr("Edit...")); + removeButton->setText(tr("Remove")); + moveUpButton->setText(tr("Move Up")); + moveDownButton->setText(tr("Move Down")); + modeLabel->setText(tr("When a deck has no playmat:")); + modeCombo->setItemText(0, tr("Always use the first playmat")); + modeCombo->setItemText(1, tr("Cycle through playmats (one per game)")); + modeCombo->setItemText(2, tr("Pick a random playmat per game")); +} diff --git a/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.h b/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.h new file mode 100644 index 000000000..1a93e0d78 --- /dev/null +++ b/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.h @@ -0,0 +1,55 @@ +#ifndef COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H +#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H + +#include +#include + +class QComboBox; +class QLabel; +class QListWidget; +class QListWidgetItem; +class QPushButton; + +/** + * @brief Dialog for editing the user-level playmat collection. + * + * The collection is the fallback used when a deck has no playmat of its own. + * It supports multiple entries and a pick mode (always first / round-robin / + * random). The dialog edits a working copy and writes it to the settings only + * when accepted. + */ +class PlaymatCollectionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PlaymatCollectionDialog(QWidget *parent = nullptr); + + void accept() override; + +private slots: + void addPlaymat(); + void editPlaymat(); + void removePlaymat(); + void movePlaymatUp(); + void movePlaymatDown(); + void selectionChanged(); + +private: + void setupUi(); + void retranslateUi(); + int currentRow() const; + void appendEntry(const StoredPlaymat &entry); + + QList playmats; ///< Working copy edited by the dialog. + QListWidget *playmatList; + QComboBox *modeCombo; + QLabel *modeLabel; + QPushButton *addButton; + QPushButton *editButton; + QPushButton *removeButton; + QPushButton *moveUpButton; + QPushButton *moveDownButton; +}; + +#endif // COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp index e040173ac..de5c7542d 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp @@ -7,6 +7,7 @@ #include "card_database_model.h" #include "playmat_preview_widget.h" +#include #include #include #include @@ -27,7 +28,6 @@ PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard, QWidget *parent) : QDialog(parent), currentCard(initialCard), currentParams(initialParams) { - setWindowTitle(tr("Playmat Settings")); setMinimumWidth(500); setupUi(); @@ -53,6 +53,8 @@ PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard, marginRSpin->setValue(initialParams.marginPctR); verticalOffsetSpin->setValue(initialParams.verticalOffset); zoomSpin->setValue(initialParams.zoom); + + retranslateUi(); } CardRef PlaymatSettingsDialog::card() const @@ -65,6 +67,11 @@ PlaymatParams PlaymatSettingsDialog::params() const return currentParams; } +bool PlaymatSettingsDialog::useAsDefault() const +{ + return useAsDefaultCheckBox->isChecked(); +} + QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, double value, double step) { auto *spin = new QDoubleSpinBox; @@ -78,7 +85,6 @@ QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, doubl void PlaymatSettingsDialog::initializeSearchBar() { searchBar = new QLineEdit; - searchBar->setPlaceholderText(tr("Type a card name...")); cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); @@ -121,36 +127,46 @@ void PlaymatSettingsDialog::setupUi() zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05); auto *form = new QFormLayout; - form->addRow(tr("Card name:"), searchBar); - form->addRow(tr("Printing:"), providerComboBox); - form->addRow(tr("Left margin (%):"), marginLSpin); - form->addRow(tr("Right margin (%):"), marginRSpin); - form->addRow(tr("Vertical offset:"), verticalOffsetSpin); - form->addRow(tr("Zoom:"), zoomSpin); + cardNameLabel = new QLabel; + printingLabel = new QLabel; + leftMarginLabel = new QLabel; + rightMarginLabel = new QLabel; + verticalOffsetLabel = new QLabel; + zoomLabel = new QLabel; + form->addRow(cardNameLabel, searchBar); + form->addRow(printingLabel, providerComboBox); + form->addRow(leftMarginLabel, marginLSpin); + form->addRow(rightMarginLabel, marginRSpin); + form->addRow(verticalOffsetLabel, verticalOffsetSpin); + form->addRow(zoomLabel, zoomSpin); - auto *controlsGroup = new QGroupBox(tr("Parameters")); + controlsGroup = new QGroupBox; controlsGroup->setLayout(form); + useAsDefaultCheckBox = new QCheckBox; + useAsDefaultCheckBox->setEnabled(false); + preview = new PlaymatPreviewWidget; auto *previewLayout = new QVBoxLayout; previewLayout->addWidget(preview); - auto *previewGroup = new QGroupBox(tr("Preview")); + previewGroup = new QGroupBox; previewGroup->setLayout(previewLayout); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - auto *removeBtn = new QPushButton(tr("Remove Playmat")); - buttons->addButton(removeBtn, QDialogButtonBox::ResetRole); + removeButton = new QPushButton; + buttons->addButton(removeButton, QDialogButtonBox::ResetRole); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - connect(removeBtn, &QPushButton::clicked, this, [this]() { + connect(removeButton, &QPushButton::clicked, this, [this]() { currentCard = CardRef{}; // empty signals removal accept(); }); auto *root = new QVBoxLayout; root->addWidget(controlsGroup); + root->addWidget(useAsDefaultCheckBox); root->addWidget(previewGroup); root->addWidget(buttons); setLayout(root); @@ -190,6 +206,8 @@ void PlaymatSettingsDialog::onCardNameChanged(const QString &name) if (name.isEmpty()) { currentPixmap = QPixmap(); preview->setPixmap(currentPixmap); + useAsDefaultCheckBox->setChecked(false); + useAsDefaultCheckBox->setEnabled(false); return; } @@ -198,10 +216,13 @@ void PlaymatSettingsDialog::onCardNameChanged(const QString &name) currentPixmap = QPixmap(); preview->setPixmap(currentPixmap); providerComboBox->clear(); + useAsDefaultCheckBox->setChecked(false); + useAsDefaultCheckBox->setEnabled(false); return; } currentCard.name = name; + useAsDefaultCheckBox->setEnabled(true); populateProviderCombo(name); @@ -254,3 +275,19 @@ void PlaymatSettingsDialog::onParamChanged() currentParams.zoom = zoomSpin->value(); preview->setParams(currentParams); } + +void PlaymatSettingsDialog::retranslateUi() +{ + setWindowTitle(tr("Playmat Settings")); + searchBar->setPlaceholderText(tr("Type a card name...")); + cardNameLabel->setText(tr("Card name:")); + printingLabel->setText(tr("Printing:")); + leftMarginLabel->setText(tr("Left margin (%):")); + rightMarginLabel->setText(tr("Right margin (%):")); + verticalOffsetLabel->setText(tr("Vertical offset:")); + zoomLabel->setText(tr("Zoom:")); + controlsGroup->setTitle(tr("Parameters")); + useAsDefaultCheckBox->setText(tr("Also set as my default playmat (for decks without one)")); + previewGroup->setTitle(tr("Preview")); + removeButton->setText(tr("Remove Playmat")); +} diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h index 5a9ea43e4..9637ad771 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h @@ -5,10 +5,14 @@ #include #include +class QCheckBox; class QComboBox; class QCompleter; -class QLineEdit; class QDoubleSpinBox; +class QGroupBox; +class QLabel; +class QLineEdit; +class QPushButton; class CardDatabaseModel; class CardDatabaseDisplayModel; class CardSearchModel; @@ -35,6 +39,12 @@ public: CardRef card() const; PlaymatParams params() const; + /** + * @brief Whether the user also wants the configured playmat saved as the + * user-level default (fallback for decks without one). + */ + bool useAsDefault() const; + private slots: void onCardNameChanged(const QString &name); void reloadPreview(); @@ -44,6 +54,7 @@ private: void setupUi(); void populateProviderCombo(const QString &cardName); void initializeSearchBar(); + void retranslateUi(); QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); QLineEdit *searchBar; @@ -57,6 +68,18 @@ private: QMetaObject::Connection pixmapUpdatedConnection; + QCheckBox *useAsDefaultCheckBox; + + QLabel *cardNameLabel; + QLabel *printingLabel; + QLabel *leftMarginLabel; + QLabel *rightMarginLabel; + QLabel *verticalOffsetLabel; + QLabel *zoomLabel; + QGroupBox *controlsGroup; + QGroupBox *previewGroup; + QPushButton *removeButton; + QDoubleSpinBox *marginLSpin; QDoubleSpinBox *marginRSpin; QDoubleSpinBox *verticalOffsetSpin; diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_utils.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_utils.cpp new file mode 100644 index 000000000..f4d7badfe --- /dev/null +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_utils.cpp @@ -0,0 +1,48 @@ +#include "playmat_settings_utils.h" + +#include +#include + +PlaymatResolution resolutionFromStoredPlaymat(const StoredPlaymat &stored) +{ + PlaymatResolution resolution; + resolution.card = {stored.name, stored.providerId}; + resolution.params = {qBound(0.0, stored.marginPctL, 0.95), qBound(0.0, stored.marginPctR, 0.95), + qBound(0.0, stored.verticalOffset, 1.0), qBound(0.1, stored.zoom, 4.0)}; + return resolution; +} + +StoredPlaymat storedFromResolution(const PlaymatResolution &resolution) +{ + StoredPlaymat stored; + stored.name = resolution.card.name; + stored.providerId = resolution.card.providerId; + stored.marginPctL = resolution.params.marginPctL; + stored.marginPctR = resolution.params.marginPctR; + stored.verticalOffset = resolution.params.verticalOffset; + stored.zoom = resolution.params.zoom; + return stored; +} + +DeckList applyUserPlaymatSettings(const DeckList &deck, const InterfaceSettings &settings) +{ + // Per-session cursor for the round-robin mode; advancing per resolution + // (per deck upload, i.e. roughly per game) cycles deterministically. + static int rotationIndex = 0; + + QList fallbackList; + const QList storedList = settings.getPlaymatFallbackList(); + fallbackList.reserve(storedList.size()); + for (const StoredPlaymat &stored : storedList) { + fallbackList.append(resolutionFromStoredPlaymat(stored)); + } + + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, resolutionFromStoredPlaymat(settings.getPlaymatOverride()), fallbackList, + static_cast(settings.getPlaymatFallbackMode()), rotationIndex); + + DeckList effectiveDeck = deck; + effectiveDeck.setPlaymatCard(resolved.card); + effectiveDeck.setPlaymatParams(resolved.params); + return effectiveDeck; +} diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_utils.h b/cockatrice/src/interface/widgets/playmat/playmat_settings_utils.h new file mode 100644 index 000000000..7b22d34dc --- /dev/null +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_utils.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_PLAYMAT_SETTINGS_UTILS_H +#define COCKATRICE_PLAYMAT_SETTINGS_UTILS_H + +#include +#include +#include + +class InterfaceSettings; + +/** + * @brief Converts a stored (settings-level) playmat entry into resolver types, + * clamping the positioning parameters to their valid ranges. + */ +PlaymatResolution resolutionFromStoredPlaymat(const StoredPlaymat &stored); + +/** + * @brief Converts a resolved playmat into a stored (settings-level) entry. + */ +StoredPlaymat storedFromResolution(const PlaymatResolution &resolution); + +/** + * @brief Applies the user-level playmat settings (override, fallback list and + * mode) to a copy of the deck, returning the deck with the effective + * playmat baked into its metadata. + * + * The resolution chain is: override > deck-configured playmat > fallback list. + * The deck passed in is never modified; the returned copy is intended for + * upload to the server so local and remote players see the same effective mat. + */ +DeckList applyUserPlaymatSettings(const DeckList &deck, const InterfaceSettings &settings); + +#endif // COCKATRICE_PLAYMAT_SETTINGS_UTILS_H diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index b78f2f69a..9fccc59cf 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -7,11 +7,15 @@ #include "../dialogs/override_printing_warning.h" #include "../interface/theme_manager.h" #include "../interface/widgets/general/background_sources.h" +#include "../playmat/playmat_collection_dialog.h" +#include "../playmat/playmat_settings_dialog.h" +#include "../playmat/playmat_settings_utils.h" #include #include #include #include +#include #include #include #include @@ -329,12 +333,27 @@ AppearanceSettingsPage::AppearanceSettingsPage() }); playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo); + // User-level playmat settings: override (force all decks) and fallback collection. + connect(&playmatOverrideEditButton, &QPushButton::clicked, this, + &AppearanceSettingsPage::openOverridePlaymatSettings); + connect(&playmatOverrideClearButton, &QPushButton::clicked, this, &AppearanceSettingsPage::clearOverridePlaymat); + connect(&playmatDefaultEditButton, &QPushButton::clicked, this, + &AppearanceSettingsPage::openPlaymatCollectionDialog); + + auto *overrideButtons = new QHBoxLayout; + overrideButtons->addWidget(&playmatOverrideEditButton); + overrideButtons->addWidget(&playmatOverrideClearButton); + auto *tableGrid = new QGridLayout; tableGrid->addWidget(&invertVerticalCoordinateCheckBox, 0, 0, 1, 2); tableGrid->addWidget(&minPlayersForMultiColumnLayoutLabel, 1, 0, 1, 1); tableGrid->addWidget(&minPlayersForMultiColumnLayoutEdit, 1, 1, 1, 1); tableGrid->addWidget(&playmatVisibilityLabel, 2, 0, 1, 1); tableGrid->addWidget(&playmatVisibilityCombo, 2, 1, 1, 1); + tableGrid->addWidget(&playmatOverrideLabel, 3, 0, 1, 1); + tableGrid->addLayout(overrideButtons, 3, 1, 1, 1); + tableGrid->addWidget(&playmatDefaultLabel, 4, 0, 1, 1); + tableGrid->addWidget(&playmatDefaultEditButton, 4, 1, 1, 1); tableGroupBox = new QGroupBox; tableGroupBox->setLayout(tableGrid); @@ -445,6 +464,42 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value) } } +void AppearanceSettingsPage::openOverridePlaymatSettings() +{ + auto &interfaceSettings = SettingsCache::instance().userInterface(); + const PlaymatResolution current = resolutionFromStoredPlaymat(interfaceSettings.getPlaymatOverride()); + PlaymatSettingsDialog dialog(current.card, current.params, this); + if (dialog.exec() == QDialog::Accepted) { + const CardRef card = dialog.card(); + if (card.isEmpty()) { + interfaceSettings.setPlaymatOverride(StoredPlaymat{}); + } else { + interfaceSettings.setPlaymatOverride(storedFromResolution({card, dialog.params()})); + } + updatePlaymatSummary(); + } +} + +void AppearanceSettingsPage::clearOverridePlaymat() +{ + SettingsCache::instance().userInterface().setPlaymatOverride(StoredPlaymat{}); + updatePlaymatSummary(); +} + +void AppearanceSettingsPage::openPlaymatCollectionDialog() +{ + PlaymatCollectionDialog dialog(this); + dialog.exec(); + updatePlaymatSummary(); +} + +void AppearanceSettingsPage::updatePlaymatSummary() +{ + const StoredPlaymat overridePlaymat = SettingsCache::instance().userInterface().getPlaymatOverride(); + playmatOverrideEditButton.setText(overridePlaymat.isEmpty() ? tr("Set...") + : tr("Change (%1)").arg(overridePlaymat.name)); +} + void AppearanceSettingsPage::retranslateUi() { themeGroupBox->setTitle(tr("Theme settings")); @@ -504,4 +559,9 @@ void AppearanceSettingsPage::retranslateUi() invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate")); minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:")); playmatVisibilityLabel.setText(tr("Playmat visibility:")); -} \ No newline at end of file + playmatOverrideLabel.setText(tr("Force playmat for all decks:")); + playmatOverrideClearButton.setText(tr("Clear")); + playmatDefaultLabel.setText(tr("Default playmats (when a deck has none):")); + playmatDefaultEditButton.setText(tr("Edit...")); + updatePlaymatSummary(); +} diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index 0311be302..911113dc5 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -24,6 +24,10 @@ private slots: void cardViewInitialRowsMaxChanged(int value); void cardViewExpandedRowsMaxChanged(int value); + void openOverridePlaymatSettings(); + void clearOverridePlaymat(); + void openPlaymatCollectionDialog(); + void updatePlaymatSummary(); private: QLabel themeLabel; @@ -61,6 +65,11 @@ private: QCheckBox invertVerticalCoordinateCheckBox; QLabel playmatVisibilityLabel; QComboBox playmatVisibilityCombo; + QLabel playmatOverrideLabel; + QPushButton playmatOverrideEditButton; + QPushButton playmatOverrideClearButton; + QLabel playmatDefaultLabel; + QPushButton playmatDefaultEditButton; QGroupBox *themeGroupBox; QGroupBox *homeTabGroupBox; QGroupBox *stylingGroupBox; diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 33494b9a9..813e80d98 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -11,6 +11,7 @@ set(HEADERS libcockatrice/deck_list/deck_list_history_manager.h libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/deck_list_memento.h + libcockatrice/deck_list/playmat_resolver.h libcockatrice/deck_list/sideboard_plan.h ) @@ -26,6 +27,7 @@ add_library( libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_node_tree.cpp + libcockatrice/deck_list/playmat_resolver.cpp libcockatrice/deck_list/sideboard_plan.cpp ) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.cpp new file mode 100644 index 000000000..246d05dfa --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.cpp @@ -0,0 +1,33 @@ +#include "playmat_resolver.h" + +#include + +PlaymatResolution resolveEffectivePlaymat(const DeckList &deck, + const PlaymatResolution &force, + const QList &fallbackList, + PlaymatFallbackMode fallbackMode, + int &rotationIndex) +{ + if (!force.card.isEmpty()) { + return force; + } + + if (!deck.getPlaymatCard().isEmpty()) { + return {deck.getPlaymatCard(), deck.getPlaymatParams()}; + } + + if (fallbackList.isEmpty()) { + return {}; + } + + switch (fallbackMode) { + case PlaymatFallbackMode::Fixed: + return fallbackList.first(); + case PlaymatFallbackMode::RoundRobin: + return fallbackList.at(rotationIndex++ % fallbackList.size()); + case PlaymatFallbackMode::Random: + return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size())); + } + + return {}; +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.h b/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.h new file mode 100644 index 000000000..66d2c3f54 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_PLAYMAT_RESOLVER_H +#define COCKATRICE_PLAYMAT_RESOLVER_H + +#include "deck_list.h" + +/** + * @brief How the user-level fallback playmat list is consulted when a deck has + * no playmat configured. + */ +enum class PlaymatFallbackMode +{ + Fixed = 0, ///< Always use the first entry of the fallback list. + RoundRobin = 1, ///< Cycle through the list, advancing one entry per resolution. + Random = 2 ///< Pick a random entry per resolution. +}; + +/** + * @struct PlaymatResolution + * @ingroup Decks + * @brief A resolved playmat (card + positioning parameters). + */ +struct PlaymatResolution +{ + CardRef card; ///< The card whose art is used as playmat. + PlaymatParams params; ///< Positioning parameters for the playmat card image. + + bool operator==(const PlaymatResolution &other) const + { + return card == other.card && params == other.params; + } +}; + +/** + * @brief Resolves the effective playmat for a deck per the resolution chain: + * force override > deck-configured playmat > fallback list > none. + * + * @param deck The deck to resolve a playmat for. + * @param force An optional user-level override; wins over everything. Pass an + * empty @ref PlaymatResolution::card to skip it. + * @param fallbackList User-level fallback playmats, consulted only when the + * deck has no configured playmat. + * @param fallbackMode How @p fallbackList is consulted (ignored when empty). + * @param rotationIndex In/out cursor for @c PlaymatFallbackMode::RoundRobin; + * advanced once per call. Unused for the other modes. + * @return The effective playmat; an empty @ref PlaymatResolution::card when + * nothing in the chain resolves. + */ +PlaymatResolution resolveEffectivePlaymat(const DeckList &deck, + const PlaymatResolution &force, + const QList &fallbackList, + PlaymatFallbackMode fallbackMode, + int &rotationIndex); + +#endif // COCKATRICE_PLAYMAT_RESOLVER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index 998dab728..737038ebd 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -1,9 +1,30 @@ #ifndef COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H +#include #include #include +/** + * @struct StoredPlaymat + * @brief A user-level playmat entry as stored in the settings, independent of + * any deck. Mirrors a deck-configured playmat (card + parameters). + */ +struct StoredPlaymat +{ + QString name; ///< Card name whose art is used as playmat. + QString providerId; ///< Printing provider id (uuid); may be empty. + double marginPctL = 0.07; ///< Left margin as fraction of card width (0.0-0.95). + double marginPctR = 0.07; ///< Right margin as fraction of card width (0.0-0.95). + double verticalOffset = 0.33; ///< Vertical position within card (0.0=top, 1.0=bottom). + double zoom = 1.0; ///< Scale factor (0.1-4.0). + + bool isEmpty() const + { + return name.isEmpty(); + } +}; + class IInterfaceSettingsProvider { public: @@ -44,6 +65,15 @@ public: [[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0; [[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0; [[nodiscard]] virtual int getPlaymatVisibility() const = 0; + + /** @brief User-level fallback playmats, used when a deck has none configured. */ + [[nodiscard]] virtual QList getPlaymatFallbackList() const = 0; + + /** @brief User-level override playmat applied to all decks, or empty to disable. */ + [[nodiscard]] virtual StoredPlaymat getPlaymatOverride() const = 0; + + /** @brief How the fallback list is consulted (0 Fixed, 1 Round-robin, 2 Random). */ + [[nodiscard]] virtual int getPlaymatFallbackMode() const = 0; }; #endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index fff39f296..a1d3568c7 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -1,5 +1,34 @@ #include "interface_settings.h" +namespace +{ +const QChar PLAYMAT_FIELD_SEP = QChar(0x1F); ///< Separator between StoredPlaymat fields. + +QString encodeStoredPlaymat(const StoredPlaymat &stored) +{ + return stored.name + PLAYMAT_FIELD_SEP + stored.providerId + PLAYMAT_FIELD_SEP + + QString::number(stored.marginPctL, 'f', 4) + PLAYMAT_FIELD_SEP + QString::number(stored.marginPctR, 'f', 4) + + PLAYMAT_FIELD_SEP + QString::number(stored.verticalOffset, 'f', 4) + PLAYMAT_FIELD_SEP + + QString::number(stored.zoom, 'f', 4); +} + +StoredPlaymat decodeStoredPlaymat(const QString &encoded) +{ + const QStringList fields = encoded.split(PLAYMAT_FIELD_SEP); + if (fields.size() != 6) { + return {}; + } + StoredPlaymat stored; + stored.name = fields.at(0); + stored.providerId = fields.at(1); + stored.marginPctL = fields.at(2).toDouble(); + stored.marginPctR = fields.at(3).toDouble(); + stored.verticalOffset = fields.at(4).toDouble(); + stored.zoom = fields.at(5).toDouble(); + return stored; +} +} // namespace + InterfaceSettings::InterfaceSettings(const QString &settingPath, QObject *parent) : SettingsManager(settingPath + "interface.ini", "interface", QString(), parent) { @@ -165,6 +194,30 @@ int InterfaceSettings::getPlaymatVisibility() const return getValue("playmatvisibility", QString(), QString(), 2).toInt(); } +QList InterfaceSettings::getPlaymatFallbackList() const +{ + const QStringList entries = getValue("playmatFallbackList", QString(), QString(), QStringList()).toStringList(); + QList result; + result.reserve(entries.size()); + for (const QString &entry : entries) { + const StoredPlaymat stored = decodeStoredPlaymat(entry); + if (!stored.isEmpty()) { + result.append(stored); + } + } + return result; +} + +StoredPlaymat InterfaceSettings::getPlaymatOverride() const +{ + return decodeStoredPlaymat(getValue("playmatOverride", QString(), QString(), QString()).toString()); +} + +int InterfaceSettings::getPlaymatFallbackMode() const +{ + return qBound(0, getValue("playmatFallbackMode", QString(), QString(), 1).toInt(), 2); +} + bool InterfaceSettings::getLifeCounterAnimationsEnabled() const { return getValue("lifeCounterAnimationsEnabled", QString(), QString(), true).toBool(); @@ -357,6 +410,33 @@ void InterfaceSettings::setPlaymatVisibility(int _visibility) emit playmatVisibilityChanged(_visibility); } +void InterfaceSettings::setPlaymatFallbackList(const QList &_fallbackList) +{ + QStringList entries; + entries.reserve(_fallbackList.size()); + for (const StoredPlaymat &stored : _fallbackList) { + entries.append(encodeStoredPlaymat(stored)); + } + setValue(entries, "playmatFallbackList"); + emit playmatSettingsChanged(); +} + +void InterfaceSettings::setPlaymatOverride(const StoredPlaymat &_override) +{ + setValue(_override.isEmpty() ? QString() : encodeStoredPlaymat(_override), "playmatOverride"); + emit playmatSettingsChanged(); +} + +void InterfaceSettings::setPlaymatFallbackMode(int _fallbackMode) +{ + const int mode = qBound(0, _fallbackMode, 2); + if (getPlaymatFallbackMode() == mode) { + return; + } + setValue(mode, "playmatFallbackMode"); + emit playmatSettingsChanged(); +} + void InterfaceSettings::setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled) { setValue(_lifeCounterAnimationsEnabled, "lifeCounterAnimationsEnabled"); diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.h b/libcockatrice_settings/libcockatrice/settings/interface_settings.h index a08dbbb2d..bedc20ef0 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -43,6 +43,9 @@ public: [[nodiscard]] bool getShowShortcuts() const override; [[nodiscard]] bool getShowGameSelectorFilterToolbar() const override; [[nodiscard]] int getPlaymatVisibility() const override; + [[nodiscard]] QList getPlaymatFallbackList() const override; + [[nodiscard]] StoredPlaymat getPlaymatOverride() const override; + [[nodiscard]] int getPlaymatFallbackMode() const override; [[nodiscard]] bool getLifeCounterAnimationsEnabled() const override; [[nodiscard]] bool getBattlefieldFlashEnabled() const override; [[nodiscard]] QStringList getUserListExpandedSections() const override; @@ -79,6 +82,9 @@ public: void setShowShortcuts(bool _showShortcuts); void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar); void setPlaymatVisibility(int _visibility); + void setPlaymatFallbackList(const QList &_fallbackList); + void setPlaymatOverride(const StoredPlaymat &_override); + void setPlaymatFallbackMode(int _fallbackMode); void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled); void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled); void setUserListExpandedSections(const QStringList §ions); @@ -94,6 +100,7 @@ signals: void showStatusBarChanged(bool state); void showGameSelectorFilterToolbarChanged(bool state); void playmatVisibilityChanged(int visibility); + void playmatSettingsChanged(); void lifeCounterAnimationsEnabledChanged(bool state); void battlefieldFlashEnabledChanged(bool state); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1c3f4c2c6..51809912b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ add_test(NAME expression_test COMMAND expression_test) add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test) add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) +add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test) add_test(NAME server_counter_test COMMAND server_counter_test) add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test) @@ -21,6 +22,7 @@ add_executable(expression_test expression_test.cpp) add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp) add_executable(test_age_formatting test_age_formatting.cpp) add_executable(password_hash_test password_hash_test.cpp) +add_executable(playmat_resolver_test playmat_resolver_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) add_executable(server_card_counter_test server_card_counter_test.cpp) add_executable(server_counter_test server_counter_test.cpp) @@ -56,6 +58,7 @@ if(NOT GTEST_FOUND) add_dependencies(clamped_arithmetic_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) + add_dependencies(playmat_resolver_test gtest) add_dependencies(deck_hash_performance_test gtest) add_dependencies(server_card_counter_test gtest) add_dependencies(server_counter_test gtest) @@ -74,6 +77,10 @@ target_link_libraries( target_link_libraries( password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) +target_link_libraries( + playmat_resolver_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} +) target_link_libraries( deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} diff --git a/tests/playmat_resolver_test.cpp b/tests/playmat_resolver_test.cpp new file mode 100644 index 000000000..eb29f14fc --- /dev/null +++ b/tests/playmat_resolver_test.cpp @@ -0,0 +1,140 @@ +#include +#include +#include + +namespace +{ + +PlaymatResolution makeResolution(const QString &name, const QString &providerId = QString()) +{ + PlaymatResolution resolution; + resolution.card = {name, providerId}; + return resolution; +} + +} // namespace + +TEST(PlaymatResolverTest, EmptyChainReturnsEmpty) +{ + DeckList deck; + int rotationIndex = 0; + const PlaymatResolution resolved = resolveEffectivePlaymat(deck, {}, {}, PlaymatFallbackMode::Fixed, rotationIndex); + EXPECT_TRUE(resolved.card.isEmpty()); +} + +TEST(PlaymatResolverTest, OverrideWinsOverDeckAndFallback) +{ + DeckList deck; + deck.setPlaymatCard({QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}); + + const PlaymatResolution force = makeResolution(QStringLiteral("Force Mat"), QStringLiteral("force-provider")); + const QList fallback = {makeResolution(QStringLiteral("Fallback Mat"))}; + + int rotationIndex = 0; + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, force, fallback, PlaymatFallbackMode::Fixed, rotationIndex); + EXPECT_EQ(resolved.card.name, QStringLiteral("Force Mat")); + EXPECT_EQ(resolved.card.providerId, QStringLiteral("force-provider")); +} + +TEST(PlaymatResolverTest, DeckWinsOverFallback) +{ + DeckList deck; + deck.setPlaymatCard({QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}); + deck.setPlaymatParams({0.1, 0.2, 0.3, 1.5}); + + const QList fallback = {makeResolution(QStringLiteral("Fallback Mat"))}; + + int rotationIndex = 0; + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, rotationIndex); + EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat")); + EXPECT_EQ(resolved.card.providerId, QStringLiteral("deck-provider")); + EXPECT_DOUBLE_EQ(resolved.params.marginPctL, 0.1); + EXPECT_DOUBLE_EQ(resolved.params.verticalOffset, 0.3); +} + +TEST(PlaymatResolverTest, FallbackUsedWhenDeckHasNone) +{ + DeckList deck; + const QList fallback = {makeResolution(QStringLiteral("Fallback Mat"))}; + + int rotationIndex = 0; + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, rotationIndex); + EXPECT_EQ(resolved.card.name, QStringLiteral("Fallback Mat")); +} + +TEST(PlaymatResolverTest, FixedAlwaysUsesFirst) +{ + DeckList deck; + const QList fallback = {makeResolution(QStringLiteral("First")), + makeResolution(QStringLiteral("Second"))}; + + int rotationIndex = 3; // ignored in Fixed mode + for (int i = 0; i < 5; ++i) { + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Fixed, rotationIndex); + EXPECT_EQ(resolved.card.name, QStringLiteral("First")); + } +} + +TEST(PlaymatResolverTest, RoundRobinCyclesAndWraps) +{ + DeckList deck; + const QList fallback = {makeResolution(QStringLiteral("First")), + makeResolution(QStringLiteral("Second")), + makeResolution(QStringLiteral("Third"))}; + + int rotationIndex = 0; + const QStringList expected = {QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third"), + QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third")}; + for (const QString &name : expected) { + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::RoundRobin, rotationIndex); + EXPECT_EQ(resolved.card.name, name); + } + EXPECT_EQ(rotationIndex, expected.size()); +} + +TEST(PlaymatResolverTest, RoundRobinRespectsExistingCursor) +{ + DeckList deck; + const QList fallback = {makeResolution(QStringLiteral("First")), + makeResolution(QStringLiteral("Second"))}; + + int rotationIndex = 5; + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::RoundRobin, rotationIndex); + EXPECT_EQ(resolved.card.name, QStringLiteral("Second")); // 5 % 2 == 1 + EXPECT_EQ(rotationIndex, 6); +} + +TEST(PlaymatResolverTest, RandomStaysWithinList) +{ + DeckList deck; + const QList fallback = {makeResolution(QStringLiteral("First")), + makeResolution(QStringLiteral("Second")), + makeResolution(QStringLiteral("Third"))}; + + int rotationIndex = 0; + for (int i = 0; i < 50; ++i) { + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackMode::Random, rotationIndex); + ASSERT_FALSE(resolved.card.name.isEmpty()); + EXPECT_TRUE(fallback.contains(resolved)); + } +} + +TEST(PlaymatResolverTest, ForceWithEmptyCardIgnoresFallbackParamsButNotFallback) +{ + DeckList deck; + deck.setPlaymatCard({QStringLiteral("Deck Mat")}); + + // An empty force entry must not mask the deck-configured playmat. + const PlaymatResolution emptyForce; + int rotationIndex = 0; + const PlaymatResolution resolved = + resolveEffectivePlaymat(deck, emptyForce, {}, PlaymatFallbackMode::Fixed, rotationIndex); + EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat")); +}