diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index f1846ecf6..09fabfbc9 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -3,7 +3,7 @@ name: Build Desktop permissions: actions: write # needed to delete entries in GHA cache (update ccache) attestations: write # needed to persist the attestation. - contents: write + contents: write # needed for e.g. vcpkg dependency graph updates id-token: write # needed for signing certificate in attestation on: @@ -440,6 +440,7 @@ jobs: CMAKE_GENERATOR: ${{ matrix.cmake_generator }} CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }} DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' + GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} @@ -450,6 +451,7 @@ jobs: USE_CCACHE: ${{ matrix.use_ccache }} VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' VCPKG_DISABLE_METRICS: 1 + VCPKG_FEATURE_FLAGS: dependencygraph run: .ci/compile.sh --server --test --vcpkg # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index dd873cfa5..4abb8210c 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -52,18 +52,14 @@ static void setupParserRules() search["Start"] = passthru; search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter { - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { - auto matchesFilter = [&deck, &info](const std::any &query) { - return std::any_cast(query)(deck, info); - }; + return [=](const DeckSearchData &data) { + auto matchesFilter = [&data](const std::any &query) { return std::any_cast(query)(data); }; return std::all_of(sv.begin(), sv.end(), matchesFilter); }; }; search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter { - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { - auto matchesFilter = [&deck, &info](const std::any &query) { - return std::any_cast(query)(deck, info); - }; + return [=](const DeckSearchData &data) { + auto matchesFilter = [&data](const std::any &query) { return std::any_cast(query)(data); }; return std::any_of(sv.begin(), sv.end(), matchesFilter); }; }; @@ -71,9 +67,7 @@ static void setupParserRules() search["QueryPart"] = passthru; search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { const auto dependent = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool { - return !dependent(deck, info); - }; + return [=](const DeckSearchData &data) -> bool { return !dependent(data); }; }; search["String"] = [](const peg::SemanticValues &sv) -> QString { @@ -125,9 +119,9 @@ static void setupParserRules() auto cardFilter = FilterString(std::any_cast(sv[0])); auto numberMatcher = sv.size() > 1 ? std::any_cast(sv[1]) : [](int count) { return count > 0; }; - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool { + return [=](const DeckSearchData &data) -> bool { int count = 0; - auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes(); + auto cardNodes = data.deck->deckList.getCardNodes(); for (auto node : cardNodes) { auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName()); if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) { @@ -146,53 +140,49 @@ static void setupParserRules() search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { - return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive); + return [=](const DeckSearchData &data) { + return data.deck->deckList.getName().contains(name, Qt::CaseInsensitive); }; }; search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { - auto filename = QFileInfo(deck->filePath).fileName(); + return [=](const DeckSearchData &data) { + auto filename = QFileInfo(data.filePath).fileName(); return filename.contains(name, Qt::CaseInsensitive); }; }; search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) { - return info.relativeFilePath.contains(name, Qt::CaseInsensitive); - }; + return [=](const DeckSearchData &data) { return data.relativeFilePath.contains(name, Qt::CaseInsensitive); }; }; search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto format = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { - auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat(); + return [=](const DeckSearchData &data) { + auto gameFormat = data.deck->deckList.getGameFormat(); return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0; }; }; search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto value = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { - auto comments = deck->deckLoader->getDeck().deckList.getComments(); + return [=](const DeckSearchData &data) { + auto comments = data.deck->deckList.getComments(); return comments.contains(value, Qt::CaseInsensitive); }; }; search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { - return deck->getDisplayName().contains(name, Qt::CaseInsensitive); - }; + return [=](const DeckSearchData &data) { return data.displayName.contains(name, Qt::CaseInsensitive); }; }; } DeckFilterString::DeckFilterString() { - filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; + filter = [](const DeckSearchData &) { return false; }; _error = "Not initialized"; } @@ -205,7 +195,7 @@ DeckFilterString::DeckFilterString(const QString &expr) _error = QString(); if (ba.isEmpty()) { - filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; }; + filter = [](const DeckSearchData &) { return true; }; return; } @@ -215,6 +205,6 @@ DeckFilterString::DeckFilterString(const QString &expr) if (!search.parse(ba.data(), filter)) { qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error); - filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; + filter = [](const DeckSearchData &) { return false; }; } -} \ No newline at end of file +} diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h index 916b629ee..90a6a17eb 100644 --- a/cockatrice/src/filters/deck_filter_string.h +++ b/cockatrice/src/filters/deck_filter_string.h @@ -7,7 +7,7 @@ #ifndef DECK_FILTER_STRING_H #define DECK_FILTER_STRING_H -#include "../interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h" +#include "../interface/deck_loader/loaded_deck.h" #include #include @@ -16,26 +16,29 @@ inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string"); /** - * Extra info relevant to filtering that isn't present in the DeckPreviewWidget + * The data a deck search expression is evaluated against. + * + * This is a data view rather than a widget pointer, so the same filter + * expression can be evaluated against a model or a live widget. */ -struct ExtraDeckSearchInfo +struct DeckSearchData { - /** - * The relative filepath starting from the deck folder - */ - QString relativeFilePath; + const LoadedDeck *deck = nullptr; ///< The loaded deck. Must not be null. + QString filePath; ///< Absolute path of the deck file. + QString displayName; ///< Deck name, or the file name if the deck has no name. + QString relativeFilePath; ///< File path relative to the deck folder. }; -typedef std::function DeckFilter; +typedef std::function DeckFilter; class DeckFilterString { public: DeckFilterString(); explicit DeckFilterString(const QString &expr); - bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const + bool check(const DeckSearchData &data) const { - return filter(deck, info); + return filter(data); } [[nodiscard]] bool valid() const diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp index a20fb1b3c..e63117e13 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -29,8 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state, { setAcceptHoverEvents(true); - connect(state, &CounterState::valueChanged, this, [this](int, int newValue) { + connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { value = newValue; + onValueChanged(oldValue, newValue); update(); }); @@ -228,3 +229,9 @@ void AbstractCounterDialog::changeValue(int diff) curValue += diff; setTextValue(QString::number(curValue)); } + +void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/) +{ + // Default: no feedback. Subclasses such as PlayerCounter override this to + // flash the counter on meaningful changes (life gain/loss). +} diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h index b319a722d..9ddcc6d58 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -35,6 +35,13 @@ protected: bool hovered = false; bool useNameForShortcut; + /** + * @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash). + * + * Called whenever the counter's value changes, before the item repaints. + */ + virtual void onValueChanged(int oldValue, int newValue); + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index d443853ce..2831f3393 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -188,6 +188,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state) AbstractCounter *widget; if (state->getName() == "life") { widget = playerTarget->addCounter(state); + connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { + if (newValue < oldValue) { + tableZoneGraphicsItem->triggerDamageShimmer(); + } + }); } else { widget = new GeneralCounter(state, player, true, this); } diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index 567f3d44d..910ee9c17 100644 --- a/cockatrice/src/game_graphics/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -1,8 +1,11 @@ #include "player_target.h" +#include "../../client/settings/cache_settings.h" #include "../../game/player/player_logic.h" #include "../../interface/pixel_map_generator.h" +#include "../game_scene.h" +#include #include #include #include @@ -21,17 +24,24 @@ QRectF PlayerCounter::boundingRect() const void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - const int radius = 8; - const qreal border = 1; - QPainterPath path(QPointF(50 - border / 2, border / 2)); - path.lineTo(radius, border / 2); - path.arcTo(border / 2, border / 2, 2 * radius, 2 * radius, 90, 90); - path.lineTo(border / 2, 30 - border / 2); - path.lineTo(50 - border / 2, 30 - border / 2); - path.closeSubpath(); + const int radius = 15; + const qreal border = 1.5; + // The box is drawn with a border-wide stroke straddling the path, so the + // visible outline spans [inset, inset + border]. Fills that must not cover + // the outline (e.g. the life-change flash) use a path inset by `border`. + const auto makePath = [](qreal inset) { + QPainterPath path(QPointF(50 - inset, inset)); + path.lineTo(radius, inset); + path.arcTo(inset, inset, 2 * radius, 2 * radius, 90, 90); + path.lineTo(inset, 30 - inset); + path.lineTo(50 - inset, 30 - inset); + path.closeSubpath(); + return path; + }; + QPainterPath path = makePath(border / 2); QPen pen(QColor(100, 100, 100)); - pen.setWidth(border); + pen.setWidthF(border); painter->setPen(pen); painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160)); @@ -45,6 +55,48 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /* painter->setFont(font); painter->setPen(Qt::white); painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value)); + + // Life-change flash: emerald on gain, red on loss, decaying over a few ticks. + if (flashAlpha > 0) { + painter->save(); + QColor flashColor = flashDelta > 0 ? QColor(52, 224, 122) : QColor(239, 68, 68); + flashColor.setAlphaF(0.45 * flashAlpha); + painter->setPen(Qt::NoPen); + painter->setBrush(flashColor); + painter->setOpacity(0.85); + painter->drawPath(makePath(border)); + painter->restore(); + } +} + +void PlayerCounter::onValueChanged(int oldValue, int newValue) +{ + flashDelta = newValue - oldValue; + if (flashDelta == 0) { + return; + } + + if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) { + flashAlpha = 0.0; + return; + } + + flashAlpha = 1.0; + flashClock.start(); + if (scene()) { + static_cast(scene())->registerAnimationItem(this); + } +} + +bool PlayerCounter::animationEvent() +{ + flashAlpha = 1.0 - flashClock.elapsed() / flashDurationMs; + if (flashAlpha <= 0.0) { + flashAlpha = 0.0; + return false; + } + update(); + return true; } PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem) diff --git a/cockatrice/src/game_graphics/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h index 67e155660..af0e9c8b7 100644 --- a/cockatrice/src/game_graphics/player/player_target.h +++ b/cockatrice/src/game_graphics/player/player_target.h @@ -7,21 +7,34 @@ #ifndef PLAYERTARGET_H #define PLAYERTARGET_H +#include "../animated_item.h" #include "../board/abstract_counter.h" #include "../board/arrow_target.h" #include "../board/graphics_item_type.h" +#include #include class PlayerLogic; -class PlayerCounter : public AbstractCounter +class PlayerCounter : public AbstractCounter, public IAnimatedItem { Q_OBJECT +protected: + void onValueChanged(int oldValue, int newValue) override; + +private: + static constexpr qreal flashDurationMs = 450.0; + + QElapsedTimer flashClock; + qreal flashAlpha = 0.0; + int flashDelta = 0; + public: PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + bool animationEvent() override; }; class PlayerTarget : public ArrowTarget diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index 4ef01853f..306e2927e 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -8,6 +8,7 @@ #include "../board/arrow_item.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" +#include "../game_scene.h" #include "../z_values.h" #include @@ -47,6 +48,31 @@ void TableZone::updateBg() update(); } +void TableZone::triggerDamageShimmer() +{ + if (!SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()) { + damageShimmerAlpha = 0.0; + return; + } + + damageShimmerAlpha = 1.0; + shimmerClock.start(); + if (scene()) { + static_cast(scene())->registerAnimationItem(this); + } +} + +bool TableZone::animationEvent() +{ + damageShimmerAlpha = 1.0 - shimmerClock.elapsed() / shimmerDurationMs; + if (damageShimmerAlpha <= 0.0) { + damageShimmerAlpha = 0.0; + return false; + } + update(); + return true; +} + QRectF TableZone::boundingRect() const { return QRectF(0, 0, width, height); @@ -77,6 +103,13 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti painter->fillRect(boundingRect(), FADE_MASK); } + // Decaying crimson wash from taking damage. + if (damageShimmerAlpha > 0.0) { + QColor shimmerColor(239, 68, 68); + shimmerColor.setAlphaF(0.22 * damageShimmerAlpha); + painter->fillRect(boundingRect(), shimmerColor); + } + paintLandDivider(painter); } diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index 0d7e58206..1836c96ff 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -8,16 +8,19 @@ #define TABLEZONE_H #include "../../game/zones/table_zone_logic.h" +#include "../animated_item.h" #include "../board/abstract_card_item.h" #include "select_zone.h" +#include + /** * @brief TableZone is the grid based rect where CardItems may be placed. * * It is the main play zone and can be customized with background images. */ //! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()). -class TableZone : public SelectZone +class TableZone : public SelectZone, public IAnimatedItem { Q_OBJECT @@ -121,6 +124,16 @@ public: */ void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + /** + Flashes the table surface after a player loses life. + + Wired up through the life counter so the battlefield glows when life drops. + */ + void triggerDamageShimmer(); + + /** @brief Decays the damage shimmer by one timer tick. */ + bool animationEvent() override; + /** Toggles the selected items as tapped. */ @@ -185,6 +198,11 @@ public: } private: + static constexpr qreal shimmerDurationMs = 450.0; + + QElapsedTimer shimmerClock; + qreal damageShimmerAlpha = 0.0; + void paintZoneOutline(QPainter *painter); void paintLandDivider(QPainter *painter); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp index 4e45f2c25..7e174a228 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp @@ -57,16 +57,16 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, gameNameFilterEdit->setText(filters.gameNameFilter); auto *gameNameFilterLabel = new QLabel(tr("Game &description:")); gameNameFilterLabel->setBuddy(gameNameFilterEdit); - creatorNameFilterEdit = new QLineEdit; - creatorNameFilterEdit->setText(filters.creatorNameFilters.join(", ")); - auto *creatorNameFilterLabel = new QLabel(tr("&Creator name:")); - creatorNameFilterLabel->setBuddy(creatorNameFilterEdit); + hostNameFilterEdit = new QLineEdit; + hostNameFilterEdit->setText(filters.hostNameFilters.join(", ")); + auto *hostNameFilterLabel = new QLabel(tr("&Host name:")); + hostNameFilterLabel->setBuddy(hostNameFilterEdit); auto *generalGrid = new QGridLayout; generalGrid->addWidget(gameNameFilterLabel, 0, 0); generalGrid->addWidget(gameNameFilterEdit, 0, 1); - generalGrid->addWidget(creatorNameFilterLabel, 1, 0); - generalGrid->addWidget(creatorNameFilterEdit, 1, 1); + generalGrid->addWidget(hostNameFilterLabel, 1, 0); + generalGrid->addWidget(hostNameFilterEdit, 1, 1); generalGrid->addWidget(maxGameAgeLabel, 2, 0); generalGrid->addWidget(maxGameAgeComboBox, 2, 1); generalGroupBox = new QGroupBox(tr("General")); @@ -193,7 +193,7 @@ GameFilterConfigs DlgFilterGames::getFilters() const hideNotBuddyCreatedGames->isChecked(), hideOpenDecklistGames->isChecked(), gameNameFilterEdit->text(), - getCreatorNameFilters(), + getHostNameFilters(), getGameTypeFilter(), maxPlayersFilterMinSpinBox->value(), maxPlayersFilterMaxSpinBox->value(), @@ -216,9 +216,9 @@ void DlgFilterGames::toggleSpectatorCheckboxEnabledness(bool spectatorsEnabled) showOnlyIfSpectatorsCanSeeHands->setDisabled(!spectatorsEnabled); } -QStringList DlgFilterGames::getCreatorNameFilters() const +QStringList DlgFilterGames::getHostNameFilters() const { - return creatorNameFilterEdit->text().split(",", Qt::SkipEmptyParts); + return hostNameFilterEdit->text().split(",", Qt::SkipEmptyParts); } QSet DlgFilterGames::getGameTypeFilter() const diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h index 447f9b16c..1cf822b89 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h @@ -35,7 +35,7 @@ private: QCheckBox *hideNotBuddyCreatedGames; QCheckBox *hideOpenDecklistGames; QLineEdit *gameNameFilterEdit; - QLineEdit *creatorNameFilterEdit; + QLineEdit *hostNameFilterEdit; QMap gameTypeFilterCheckBoxes; QSpinBox *maxPlayersFilterMinSpinBox; QSpinBox *maxPlayersFilterMaxSpinBox; @@ -50,7 +50,7 @@ private: const GamesProxyModel *gamesProxyModel; const QMap gameAgeMap; - [[nodiscard]] QStringList getCreatorNameFilters() const; + [[nodiscard]] QStringList getHostNameFilters() const; [[nodiscard]] QSet getGameTypeFilter() const; [[nodiscard]] QTime getMaxGameAge() const; [[nodiscard]] bool getShowSpectatorPasswordProtected() const; diff --git a/cockatrice/src/interface/widgets/server/game_filter_configs.h b/cockatrice/src/interface/widgets/server/game_filter_configs.h index 0ece7e00c..5cb048669 100644 --- a/cockatrice/src/interface/widgets/server/game_filter_configs.h +++ b/cockatrice/src/interface/widgets/server/game_filter_configs.h @@ -19,7 +19,7 @@ struct GameFilterConfigs bool hideNotBuddyCreatedGames = false; bool hideOpenDecklistGames = false; QString gameNameFilter = ""; - QStringList creatorNameFilters = {}; + QStringList hostNameFilters = {}; QSet gameTypeFilter = {}; int maxPlayersFilterMin = DEFAULT_MAX_PLAYERS_MIN; int maxPlayersFilterMax = DEFAULT_MAX_PLAYERS_MAX; diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index 8e623d392..ce10bee71 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -17,13 +17,27 @@ enum GameListColumn ROOM, CREATED, DESCRIPTION, - CREATOR, + HOST, GAME_TYPE, RESTRICTIONS, PLAYERS, SPECTATORS }; +namespace +{ +/** + * @brief Returns the user info of the game's current host, falling back to the creator. + * + * The server only sends host_info once a host transfer has happened, so older + * servers and freshly created games fall back to the original creator. + */ +const ServerInfo_User &getGameHost(const ServerInfo_Game &game) +{ + return game.has_host_info() ? game.host_info() : game.creator_info(); +} +} // namespace + const QString GamesModel::getGameCreatedString(const int secs) { static const QTime zeroTime{0, 0}; @@ -110,16 +124,16 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const default: return QVariant(); } - case CREATOR: { + case HOST: { switch (role) { case SORT_ROLE: case Qt::DisplayRole: - return QString::fromStdString(gameentry.creator_info().name()); + return QString::fromStdString(getGameHost(gameentry).name()); case Qt::DecorationRole: { - return UserLevelPixmapGenerator::generateIcon( - 13, UserLevelFlags(gameentry.creator_info().user_level()), - gameentry.creator_info().pawn_colors(), false, - QString::fromStdString(gameentry.creator_info().privlevel())); + const ServerInfo_User &host = getGameHost(gameentry); + return UserLevelPixmapGenerator::generateIcon(13, UserLevelFlags(host.user_level()), + host.pawn_colors(), false, + QString::fromStdString(host.privlevel())); } default: return QVariant(); @@ -233,8 +247,8 @@ QVariant GamesModel::headerData(int section, Qt::Orientation /*orientation*/, in } case DESCRIPTION: return tr("Description"); - case CREATOR: - return tr("Creator"); + case HOST: + return tr("Host"); case GAME_TYPE: return tr("Type"); case RESTRICTIONS: @@ -347,7 +361,7 @@ void GamesProxyModel::loadFilterParameters(const QMap &allGameType gameFilters.isHideFullGames(), gameFilters.isHideGamesThatStarted(), gameFilters.isHidePasswordProtectedGames(), gameFilters.isHideNotBuddyCreatedGames(), gameFilters.isHideOpenDecklistGames(), gameFilters.getGameNameFilter(), - gameFilters.getCreatorNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(), + gameFilters.getHostNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(), gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(), gameFilters.isShowOnlyIfSpectatorsCanWatch(), gameFilters.isShowSpectatorPasswordProtected(), gameFilters.isShowOnlyIfSpectatorsCanChat(), gameFilters.isShowOnlyIfSpectatorsCanSeeHands()}); @@ -364,7 +378,7 @@ void GamesProxyModel::saveFilterParameters(const QMap &allGameType gameFilters.setHideNotBuddyCreatedGames(filters.hideNotBuddyCreatedGames); gameFilters.setHideOpenDecklistGames(filters.hideOpenDecklistGames); gameFilters.setGameNameFilter(filters.gameNameFilter); - gameFilters.setCreatorNameFilters(filters.creatorNameFilters); + gameFilters.setHostNameFilters(filters.hostNameFilters); QMapIterator gameTypeIterator(allGameTypes); while (gameTypeIterator.hasNext()) { @@ -409,11 +423,11 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const return false; } if (filters.hideIgnoredUserGames && - userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) { + userListProxy->isUserIgnored(QString::fromStdString(getGameHost(game).name()))) { return false; } if (filters.hideNotBuddyCreatedGames && - !userListProxy->isUserBuddy(QString::fromStdString(game.creator_info().name()))) { + !userListProxy->isUserBuddy(QString::fromStdString(getGameHost(game).name()))) { return false; } if (filters.hideFullGames && game.player_count() == game.max_players()) { @@ -435,10 +449,10 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const return false; } } - if (!filters.creatorNameFilters.isEmpty()) { + if (!filters.hostNameFilters.isEmpty()) { bool found = false; - for (const auto &createNameFilter : filters.creatorNameFilters) { - if (QString::fromStdString(game.creator_info().name()).contains(createNameFilter, Qt::CaseInsensitive)) { + for (const auto &hostNameFilter : filters.hostNameFilters) { + if (QString::fromStdString(getGameHost(game).name()).contains(hostNameFilter, Qt::CaseInsensitive)) { found = true; } } diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index fa6de81c2..3fa56dd48 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -116,6 +116,15 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::setTapAnimation); + lifeCounterAnimationsCheckBox.setChecked( + SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()); + connect(&lifeCounterAnimationsCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setLifeCounterAnimationsEnabled); + + battlefieldFlashCheckBox.setChecked(SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()); + connect(&battlefieldFlashCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setBattlefieldFlashEnabled); + connect(&enableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::enableAllAnimations); connect(&disableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::disableAllAnimations); @@ -123,6 +132,8 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() animationGrid->addWidget(&enableAllAnimationsButton, 0, 0); animationGrid->addWidget(&disableAllAnimationsButton, 0, 1); animationGrid->addWidget(&tapAnimationCheckBox, 1, 0); + animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 2, 0); + animationGrid->addWidget(&battlefieldFlashCheckBox, 3, 0); animationGroupBox = new QGroupBox; animationGroupBox->setLayout(animationGrid); @@ -276,11 +287,15 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) void UserInterfaceSettingsPage::enableAllAnimations() { tapAnimationCheckBox.setChecked(true); + lifeCounterAnimationsCheckBox.setChecked(true); + battlefieldFlashCheckBox.setChecked(true); } void UserInterfaceSettingsPage::disableAllAnimations() { tapAnimationCheckBox.setChecked(false); + lifeCounterAnimationsCheckBox.setChecked(false); + battlefieldFlashCheckBox.setChecked(false); } void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() @@ -328,6 +343,8 @@ void UserInterfaceSettingsPage::retranslateUi() enableAllAnimationsButton.setText(tr("&Enable all animations")); disableAllAnimationsButton.setText(tr("&Disable all animations")); tapAnimationCheckBox.setText(tr("&Tap/untap animation")); + lifeCounterAnimationsCheckBox.setText(tr("Life counter flash")); + battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage")); deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default")); visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby")); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index f98e723b8..f18ab8ccf 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -40,6 +40,8 @@ private: QPushButton enableAllAnimationsButton; QPushButton disableAllAnimationsButton; QCheckBox tapAnimationCheckBox; + QCheckBox lifeCounterAnimationsCheckBox; + QCheckBox battlefieldFlashCheckBox; QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel; QComboBox visualDeckStoragePromptForConversionSelector; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 07ee55105..0580126c4 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -73,10 +73,14 @@ static QString toRelativeFilepath(const QString &filePath) void VisualDeckStorageSearchWidget::filterWidgets(QList widgets, const QString &searchText) { - auto filterString = DeckFilterString(searchText); + const auto filterString = DeckFilterString(searchText); for (auto widget : widgets) { - QString relativeFilePath = toRelativeFilepath(widget->filePath); - widget->filteredBySearch = !filterString.check(widget, {relativeFilePath}); + const DeckSearchData searchData{.deck = &widget->deckLoader->getDeck(), + .filePath = widget->filePath, + .displayName = widget->getDisplayName(), + .relativeFilePath = toRelativeFilepath(widget->filePath)}; + + widget->filteredBySearch = !filterString.check(searchData); } } diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index ab2caa0d7..1f75d3d33 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -39,6 +39,8 @@ public: [[nodiscard]] virtual bool getShowStatusBar() const = 0; [[nodiscard]] virtual bool getShowShortcuts() const = 0; [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; + [[nodiscard]] virtual bool getLifeCounterAnimationsEnabled() const = 0; + [[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0; }; #endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 4761199e5..b9e548653 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -526,10 +526,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, if (broadcastUpdate) { ServerInfo_Game gameInfo; - gameInfo.set_room_id(room->getId()); - gameInfo.set_game_id(gameId); - gameInfo.set_player_count(getPlayerCount()); - gameInfo.set_spectators_count(getSpectatorCount()); + getInfo(gameInfo); emit gameInfoChanged(gameInfo); } @@ -588,10 +585,7 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve } ServerInfo_Game gameInfo; - gameInfo.set_room_id(room->getId()); - gameInfo.set_game_id(gameId); - gameInfo.set_player_count(getPlayerCount()); - gameInfo.set_spectators_count(getSpectatorCount()); + getInfo(gameInfo); emit gameInfoChanged(gameInfo); } @@ -847,6 +841,12 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_player_count(getPlayerCount()); result.set_started(gameStarted); result.mutable_creator_info()->CopyFrom(*getCreatorInfo()); + const Server_AbstractParticipant *host = participants.value(hostId, nullptr); + if (host != nullptr) { + result.mutable_host_info()->CopyFrom(*host->getUserInfo()); + } else { + result.mutable_host_info()->CopyFrom(*getCreatorInfo()); + } result.set_only_buddies(onlyBuddies); result.set_only_registered(onlyRegistered); result.set_spectators_allowed(getSpectatorsAllowed()); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto index 9a56e034c..9989ae18a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -62,4 +62,7 @@ message ServerInfo_Game { // whether the game is closed. Closed games are finished and can't be interacted with optional bool closed = 52; + + // the current host of the game, which may differ from the creator after a host transfer + optional ServerInfo_User host_info = 53; } diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp index ad972b433..0edba44dd 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -104,14 +104,14 @@ QString GameFiltersSettings::getGameNameFilter() const return getValue("gameNameFilter").toString(); } -void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) +void GameFiltersSettings::setHostNameFilters(QStringList hostName) { - setValue(creatorName, "creatorNameFilter"); + setValue(hostName, "hostNameFilter"); } -QStringList GameFiltersSettings::getCreatorNameFilters() const +QStringList GameFiltersSettings::getHostNameFilters() const { - return getValue("creatorNameFilter").toStringList(); + return getValue("hostNameFilter").toStringList(); } void GameFiltersSettings::setMinPlayers(int min) diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h index 24f582007..11480d483 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h @@ -24,7 +24,7 @@ public: bool isHideNotBuddyCreatedGames() const; bool isHideOpenDecklistGames() const; QString getGameNameFilter() const; - QStringList getCreatorNameFilters() const; + QStringList getHostNameFilters() const; int getMinPlayers() const; int getMaxPlayers() const; QTime getMaxGameAge() const; @@ -42,7 +42,7 @@ public: void setHidePasswordProtectedGames(bool hide); void setHideNotBuddyCreatedGames(bool hide); void setGameNameFilter(QString gameName); - void setCreatorNameFilters(QStringList creatorName); + void setHostNameFilters(QStringList hostName); void setMinPlayers(int min); void setMaxPlayers(int max); void setMaxGameAge(const QTime &maxGameAge); diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index 29c57c57e..4dfc26417 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -160,6 +160,16 @@ bool InterfaceSettings::getShowGameSelectorFilterToolbar() const return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool(); } +bool InterfaceSettings::getLifeCounterAnimationsEnabled() const +{ + return getValue("lifeCounterAnimationsEnabled", QString(), QString(), true).toBool(); +} + +bool InterfaceSettings::getBattlefieldFlashEnabled() const +{ + return getValue("battlefieldFlashEnabled", QString(), QString(), true).toBool(); +} + void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus) { setValue(_useTearOffMenus, "useTearOffMenus"); @@ -326,3 +336,15 @@ void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorF setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar"); emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar); } + +void InterfaceSettings::setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled) +{ + setValue(_lifeCounterAnimationsEnabled, "lifeCounterAnimationsEnabled"); + emit lifeCounterAnimationsEnabledChanged(_lifeCounterAnimationsEnabled); +} + +void InterfaceSettings::setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled) +{ + setValue(_battlefieldFlashEnabled, "battlefieldFlashEnabled"); + emit battlefieldFlashEnabledChanged(_battlefieldFlashEnabled); +} diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.h b/libcockatrice_settings/libcockatrice/settings/interface_settings.h index 982976310..df254eb09 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -42,6 +42,8 @@ public: [[nodiscard]] bool getShowStatusBar() const override; [[nodiscard]] bool getShowShortcuts() const override; [[nodiscard]] bool getShowGameSelectorFilterToolbar() const override; + [[nodiscard]] bool getLifeCounterAnimationsEnabled() const override; + [[nodiscard]] bool getBattlefieldFlashEnabled() const override; void setUseTearOffMenus(bool _useTearOffMenus); void setCardViewInitialRowsMax(int _cardViewInitialRowsMax); @@ -74,6 +76,8 @@ public: void setShowStatusBar(bool _showStatusBar); void setShowShortcuts(bool _showShortcuts); void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar); + void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled); + void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled); signals: void useTearOffMenusChanged(bool state); @@ -85,6 +89,8 @@ signals: void tallyTypeChanged(int type); void showStatusBarChanged(bool state); void showGameSelectorFilterToolbarChanged(bool state); + void lifeCounterAnimationsEnabledChanged(bool state); + void battlefieldFlashEnabledChanged(bool state); public: explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr);