mirror of
https://github.com/Cockatrice/Cockatrice
synced 2026-08-24 00:26:08 -04:00
[Game] Playmats (#7101)
* [Game] Playmats Took 19 seconds Took 1 minute * [Playmats] Add fixed override and configurable fallbacks to settings. Took 29 minutes Took 43 seconds * Add main to test. Took 1 minute Took 29 seconds * Move settings to own group Took 11 minutes * Some attempts to refresh macOS compositor Took 2 minutes * Try something else Took 17 minutes * Don't manipulate live list Took 11 minutes * Change things about resolution, address comments. Took 45 minutes Took 12 minutes * Comments. Took 14 minutes Took 8 seconds * Re-order settings menu location Took 2 minutes * Rename PlaymatResolution to Info and add enums Took 8 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
74a454552a
commit
9eafd90a91
52 changed files with 1931 additions and 28 deletions
|
|
@ -240,6 +240,10 @@ 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/quick_settings/settings_button_widget.cpp
|
||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||
src/interface/widgets/replay/replay_manager.cpp
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
emit playerJoined(prop);
|
||||
}
|
||||
player->processPlayerInfo(playerInfo);
|
||||
// Extract playmat from player properties for opponent display
|
||||
if (prop.has_playmat_params()) {
|
||||
player->setPlaymatFromProperties(prop);
|
||||
}
|
||||
if (player->getPlayerInfo()->getLocal()) {
|
||||
emit localPlayerDeckSelected(player, playerId, playerInfo);
|
||||
} else {
|
||||
|
|
@ -351,6 +355,11 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties
|
|||
const ServerInfo_PlayerProperties &prop = event.player_properties();
|
||||
emit playerPropertiesChanged(prop, eventPlayerId);
|
||||
|
||||
// Update playmat from player properties
|
||||
if (prop.has_playmat_params()) {
|
||||
player->setPlaymatFromProperties(prop);
|
||||
}
|
||||
|
||||
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
|
||||
switch (contextType) {
|
||||
case GameEventContext::READY_START: {
|
||||
|
|
|
|||
|
|
@ -250,6 +250,22 @@ void PlayerLogic::setDeck(const DeckList &_deck)
|
|||
emit deckChanged();
|
||||
}
|
||||
|
||||
void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
|
||||
{
|
||||
if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
|
||||
const auto &pp = props.playmat_params();
|
||||
remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
|
||||
remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
|
||||
qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
|
||||
hasRemotePlaymat = true;
|
||||
} else {
|
||||
remotePlaymatCard = CardRef{};
|
||||
remotePlaymatParams = PlaymatParams{};
|
||||
hasRemotePlaymat = false;
|
||||
}
|
||||
emit playmatChanged();
|
||||
}
|
||||
|
||||
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
|
||||
{
|
||||
return addCounter(counter.id(), QString::fromStdString(counter.name()),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "../zones/table_zone_logic.h"
|
||||
#include "player_event_handler.h"
|
||||
#include "player_info.h"
|
||||
#include "player_manager.h"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QLoggingCategory>
|
||||
|
|
@ -72,6 +73,8 @@ signals:
|
|||
const QList<const ServerInfo_Card *> &cardList,
|
||||
bool withWritePermission);
|
||||
void deckChanged();
|
||||
/** @brief Emitted when the remote playmat (card/params) is updated from player properties. */
|
||||
void playmatChanged();
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void requestCardMenuUpdate(const CardItem *card);
|
||||
void counterAdded(CounterState *state);
|
||||
|
|
@ -226,6 +229,20 @@ public:
|
|||
|
||||
void setZoneId(int _zoneId);
|
||||
|
||||
void setPlaymatFromProperties(const ServerInfo_PlayerProperties &props);
|
||||
const CardRef &getRemotePlaymatCard() const
|
||||
{
|
||||
return remotePlaymatCard;
|
||||
}
|
||||
const PlaymatParams &getRemotePlaymatParams() const
|
||||
{
|
||||
return remotePlaymatParams;
|
||||
}
|
||||
bool getHasRemotePlaymat() const
|
||||
{
|
||||
return hasRemotePlaymat;
|
||||
}
|
||||
|
||||
private:
|
||||
AbstractGame *game;
|
||||
PlayerInfo *playerInfo;
|
||||
|
|
@ -243,6 +260,11 @@ private:
|
|||
|
||||
bool dialogSemaphore;
|
||||
QList<CardItem *> cardsToDelete;
|
||||
|
||||
// Playmat from player properties (for opponent display)
|
||||
CardRef remotePlaymatCard;
|
||||
PlaymatParams remotePlaymatParams;
|
||||
bool hasRemotePlaymat = false;
|
||||
};
|
||||
|
||||
class AnnotationDialog : public QInputDialog
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@
|
|||
#include <QMessageBox>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
|
|
@ -100,6 +103,9 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
|
||||
this, &DeckViewContainer::setVisualDeckStorageExists);
|
||||
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
|
||||
&DeckViewContainer::onPlaymatSettingsChanged);
|
||||
|
||||
switchToDeckSelectView();
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +283,8 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
|||
|
||||
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||
{
|
||||
currentDeck = deck;
|
||||
|
||||
QString deckString = deck.writeToString_Native();
|
||||
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
|
|
@ -289,6 +297,52 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
|||
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
||||
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
void DeckViewContainer::resolveAndSendPlaymat()
|
||||
{
|
||||
if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &settings = SettingsCache::instance().userInterface();
|
||||
const auto fallbackBehavior = static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior());
|
||||
|
||||
QList<PlaymatInfo> fallbackList = settings.getPlaymatFallbackList();
|
||||
|
||||
// In random mode with 2+ entries, remove the last-resolved mat to avoid repeats.
|
||||
if (fallbackBehavior == PlaymatFallbackModeRandom && fallbackList.size() > 1) {
|
||||
fallbackList.removeAll(lastResolvedPlaymat);
|
||||
}
|
||||
|
||||
const PlaymatInfo resolved =
|
||||
resolvePlaymatForDeck(currentDeck, fallbackList, static_cast<PlaymatMode>(settings.getPlaymatMode()),
|
||||
fallbackBehavior, playmatRotationIndex);
|
||||
|
||||
lastResolvedPlaymat = resolved;
|
||||
|
||||
Command_SetPlaymat playmatCmd;
|
||||
auto *pp = playmatCmd.mutable_playmat_params();
|
||||
pp->set_card_name(resolved.card.name.toStdString());
|
||||
pp->set_card_provider_id(resolved.card.providerId.toStdString());
|
||||
pp->set_margin_pct_l(resolved.params.marginPctL);
|
||||
pp->set_margin_pct_r(resolved.params.marginPctR);
|
||||
pp->set_vertical_offset(resolved.params.verticalOffset);
|
||||
pp->set_zoom(resolved.params.zoom);
|
||||
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::onPlaymatSettingsChanged()
|
||||
{
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
void DeckViewContainer::advancePlaymatRotation()
|
||||
{
|
||||
playmatRotationIndex++;
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadRemoteDeck()
|
||||
|
|
@ -379,6 +433,10 @@ void DeckViewContainer::sideboardPlanChanged()
|
|||
*/
|
||||
void DeckViewContainer::sendReadyStartCommand(bool ready)
|
||||
{
|
||||
if (ready) {
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_ready(ready);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
||||
|
|
@ -416,6 +474,7 @@ void DeckViewContainer::setSideboardLocked(bool locked)
|
|||
|
||||
void DeckViewContainer::setDeck(const DeckList &deck)
|
||||
{
|
||||
currentDeck = deck;
|
||||
deckView->setDeck(deck);
|
||||
switchToDeckLoadedView();
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ private:
|
|||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
TabGame *parentGame;
|
||||
int playerId;
|
||||
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
|
||||
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
|
||||
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
|
||||
|
||||
void tryCreateVisualDeckStorageWidget();
|
||||
void sendReadyStartCommand(bool ready);
|
||||
|
|
@ -75,6 +78,7 @@ private slots:
|
|||
void sideboardLockButtonClicked();
|
||||
void updateSideboardLockButtonText();
|
||||
void refreshShortcuts();
|
||||
void onPlaymatSettingsChanged();
|
||||
signals:
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void notIdle();
|
||||
|
|
@ -87,6 +91,8 @@ public:
|
|||
void setSideboardLocked(bool locked);
|
||||
void setDeck(const DeckList &deck);
|
||||
void setVisualDeckStorageExists(bool exists);
|
||||
void advancePlaymatRotation();
|
||||
void resolveAndSendPlaymat();
|
||||
|
||||
public slots:
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
|
|
|
|||
|
|
@ -252,17 +252,27 @@ void GameScene::adjustPlayerRotation(int rotationAdjustment)
|
|||
*/
|
||||
void GameScene::rearrange()
|
||||
{
|
||||
int firstPlayerIndex = 0;
|
||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||
if (rearranging) {
|
||||
needsReArrange = true;
|
||||
return;
|
||||
}
|
||||
rearranging = true;
|
||||
do {
|
||||
needsReArrange = false;
|
||||
|
||||
int columns = determineColumnCount(playersPlaying.size());
|
||||
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
||||
int firstPlayerIndex = 0;
|
||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||
|
||||
phasesToolbar->setHeight(sceneSize.height());
|
||||
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
||||
int columns = determineColumnCount(playersPlaying.size());
|
||||
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
||||
|
||||
processViewSizeChange(viewSize);
|
||||
phasesToolbar->setHeight(sceneSize.height());
|
||||
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
||||
|
||||
processViewSizeChange(viewSize);
|
||||
} while (needsReArrange);
|
||||
rearranging = false;
|
||||
}
|
||||
|
||||
// ---------- View Size ----------
|
||||
|
|
@ -459,8 +469,14 @@ void GameScene::resizeColumnsAndPlayers(const QList<qreal> &minWidthByColumn, qr
|
|||
qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size();
|
||||
qreal newx = phasesToolbar->getWidth();
|
||||
|
||||
for (int col = 0; col < playersByColumn.size(); ++col) {
|
||||
for (PlayerGraphicsItem *player : playersByColumn[col]) {
|
||||
// Snapshot the columns: resizing a player's table can synchronously trigger
|
||||
// GameScene::rearrange (table width -> sizeChanged -> updateBoundingRect ->
|
||||
// sizeChanged -> rearrange), and rearrange rebuilds playersByColumn. Iterating
|
||||
// the live container across that re-entrant call would use invalidated iterators.
|
||||
const QList<QList<PlayerGraphicsItem *>> columns = playersByColumn;
|
||||
|
||||
for (int col = 0; col < columns.size(); ++col) {
|
||||
for (PlayerGraphicsItem *player : columns[col]) {
|
||||
player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn);
|
||||
player->setPos(newx, player->y());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ private:
|
|||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
|
||||
/**
|
||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
|
|||
}
|
||||
|
||||
selectionOrigin = _selectionOrigin;
|
||||
previousBandRect = QRect();
|
||||
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
||||
rubberBand->show();
|
||||
}
|
||||
|
|
@ -128,7 +129,17 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
|
|||
|
||||
QPoint cursor = cursorPoint.toPoint();
|
||||
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
|
||||
|
||||
rubberBand->setGeometry(rect);
|
||||
if (viewport()) {
|
||||
// Repaint the union of the previous and current band rects: the vacated
|
||||
// strip of a child widget is not reliably invalidated on all platforms
|
||||
// (notably macOS), leaving stale pixels under the selection.
|
||||
QRect dirty = previousBandRect.isNull() ? rect : previousBandRect.united(rect);
|
||||
dirty.adjust(-1, -1, 1, 1);
|
||||
viewport()->update(dirty);
|
||||
previousBandRect = rect;
|
||||
}
|
||||
|
||||
if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
|
||||
dragCountLabel->hide();
|
||||
|
|
@ -171,7 +182,13 @@ void GameView::stopRubberBand()
|
|||
return;
|
||||
}
|
||||
|
||||
// Same rationale as resizeRubberBand: repaint the last known band area
|
||||
// since hiding a child widget doesn't reliably invalidate its region.
|
||||
rubberBand->hide();
|
||||
if (viewport() && !previousBandRect.isNull()) {
|
||||
viewport()->update(previousBandRect.adjusted(-1, -1, 1, 1));
|
||||
previousBandRect = QRect();
|
||||
}
|
||||
dragCountLabel->hide();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ private:
|
|||
QWidget *tallyContainer;
|
||||
QGridLayout *tallyLayout;
|
||||
QPointF selectionOrigin;
|
||||
QRect previousBandRect; ///< Last rubber-band rect for targeted repaint
|
||||
QList<TallyRow> cachedTallyRows; ///< Cached entries to avoid redundant rebuilds
|
||||
|
||||
QSize rebuildTallyLabels(const QList<TallyRow> &entries);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "player_graphics_item.h"
|
||||
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
#include "../board/counter_general.h"
|
||||
|
|
@ -13,6 +16,9 @@
|
|||
#include "player_dialogs.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||
|
|
@ -28,6 +34,10 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
|
||||
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
|
||||
connect(player, &PlayerLogic::deckChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||
connect(player, &PlayerLogic::playmatChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatVisibilityChanged, this,
|
||||
[this](int) { updatePlaymat(); });
|
||||
|
||||
playerMenu = new PlayerMenu(this);
|
||||
|
||||
|
|
@ -67,6 +77,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
|
||||
connect(this, &PlayerGraphicsItem::playmatChanged, tableZoneGraphicsItem, &TableZone::onPlaymatChanged);
|
||||
connect(this, &PlayerGraphicsItem::playmatChanged, stackZoneGraphicsItem, &StackZone::onPlaymatChanged);
|
||||
|
||||
updateBoundingRect();
|
||||
|
||||
rearrangeZones();
|
||||
|
|
@ -112,7 +125,6 @@ void PlayerGraphicsItem::initializeZones()
|
|||
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
|
||||
|
||||
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
|
||||
|
||||
stackZoneGraphicsItem =
|
||||
|
|
@ -155,10 +167,61 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
|
|||
return result;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::paint(QPainter * /*painter*/,
|
||||
const QStyleOptionGraphicsItem * /*option*/,
|
||||
QWidget * /*widget*/)
|
||||
void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
|
||||
{
|
||||
if (!hasPlaymat || playmatPixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the combined bounding rect of stack + table zones
|
||||
QPointF stackPos = stackZoneGraphicsItem->pos();
|
||||
QPointF tablePos = tableZoneGraphicsItem->pos();
|
||||
QSizeF stackSize = stackZoneGraphicsItem->boundingRect().size();
|
||||
QSizeF tableSize = tableZoneGraphicsItem->boundingRect().size();
|
||||
|
||||
// Combined area: from stack left edge to table right edge
|
||||
double combinedLeft = qMin(stackPos.x(), tablePos.x());
|
||||
double combinedTop = qMin(stackPos.y(), tablePos.y());
|
||||
double combinedRight = qMax(stackPos.x() + stackSize.width(), tablePos.x() + tableSize.width());
|
||||
double combinedBottom = qMax(stackPos.y() + stackSize.height(), tablePos.y() + tableSize.height());
|
||||
|
||||
QRectF combinedArea(combinedLeft, combinedTop, combinedRight - combinedLeft, combinedBottom - combinedTop);
|
||||
|
||||
const QRectF srcRect = computeArtSourceRect(playmatPixmap.size(), playmatParams);
|
||||
const QRectF dstRect = coverFitRect(combinedArea, srcRect.size());
|
||||
|
||||
painter->save();
|
||||
painter->setClipRect(combinedArea);
|
||||
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
|
||||
// Render from a down-scaled copy of the art so the full-resolution source
|
||||
// pixmap is never re-sampled at a tiny device size (also much cheaper than
|
||||
// scaling it on every frame).
|
||||
const QPixmap scaledPixmap = scaledPlaymatFor(srcRect, painter->worldTransform().mapRect(dstRect).size());
|
||||
painter->drawPixmap(dstRect, scaledPixmap, QRectF(scaledPixmap.rect()));
|
||||
|
||||
painter->restore();
|
||||
|
||||
if (!playmatAttribution.isEmpty()) {
|
||||
paintArtAttribution(*painter, combinedArea, playmatAttribution, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap PlayerGraphicsItem::scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize)
|
||||
{
|
||||
// Bucket the render size so the source pixmap is re-scaled at most once per
|
||||
// zoom step instead of once per frame.
|
||||
constexpr int bucketSize = 32;
|
||||
const QSize target = QSize(qMax(1, qRound(deviceDstSize.width() / bucketSize) * bucketSize),
|
||||
qMax(1, qRound(deviceDstSize.height() / bucketSize) * bucketSize))
|
||||
.boundedTo(srcRect.toAlignedRect().size());
|
||||
|
||||
if (scaledPlaymatKey != target) {
|
||||
const QPixmap crop = playmatPixmap.copy(srcRect.toAlignedRect());
|
||||
scaledPlaymatPixmap = crop.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
scaledPlaymatKey = target;
|
||||
}
|
||||
return scaledPlaymatPixmap;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
||||
|
|
@ -303,3 +366,100 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
|
||||
emit sizeChanged();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::updatePlaymat()
|
||||
{
|
||||
int visibility = SettingsCache::instance().userInterface().getPlaymatVisibility();
|
||||
|
||||
// "Don't use playmats" — never show
|
||||
if (visibility == PlaymatVisibilityNone) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
// "Show own playmat only" — hide playmats for remote players
|
||||
if (visibility == PlaymatVisibilityOwnOnly && !player->getPlayerInfo()->getLocal()) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
CardRef playmatCard;
|
||||
PlaymatParams params;
|
||||
|
||||
if (player->getHasRemotePlaymat()) {
|
||||
// Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
|
||||
playmatCard = player->getRemotePlaymatCard();
|
||||
params = player->getRemotePlaymatParams();
|
||||
} else if (player->getPlayerInfo()->getLocal()) {
|
||||
// Local player without a server broadcast yet: apply the full
|
||||
// settings-based resolution chain (mode, fallback list, behavior).
|
||||
const auto &settings = SettingsCache::instance().userInterface();
|
||||
const PlaymatInfo resolved = resolvePlaymatForDeck(
|
||||
player->getDeck(), settings.getPlaymatFallbackList(), static_cast<PlaymatMode>(settings.getPlaymatMode()),
|
||||
static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior()), 0);
|
||||
playmatCard = resolved.card;
|
||||
params = resolved.params;
|
||||
} else {
|
||||
// Opponent without a server broadcast: use the deck-embedded playmat.
|
||||
const DeckList &deck = player->getDeck();
|
||||
const PlaymatInfo &deckPlaymat = deck.getPlaymat();
|
||||
if (!deckPlaymat.card.isEmpty()) {
|
||||
playmatCard = deckPlaymat.card;
|
||||
params = deckPlaymat.params;
|
||||
}
|
||||
}
|
||||
|
||||
if (playmatCard.isEmpty()) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
playmatParams = params;
|
||||
scaledPlaymatKey = QSize(); // the art crop depends on the params, drop any cached scale
|
||||
|
||||
ExactCard card = CardDatabaseManager::query()->getCard(playmatCard);
|
||||
if (!card) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
playmatAttribution = buildArtAttribution(card);
|
||||
|
||||
QPixmap fullRes;
|
||||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (fullRes.isNull()) {
|
||||
disconnect(playmatPixmapConnection);
|
||||
CardInfo *cardInfo = card.getCardPtr().data();
|
||||
if (cardInfo) {
|
||||
playmatPixmapConnection =
|
||||
connect(cardInfo, &CardInfo::pixmapUpdated, this, &PlayerGraphicsItem::onPlaymatPixmapReady);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPlaymat) {
|
||||
hasPlaymat = true;
|
||||
emit playmatChanged(true);
|
||||
}
|
||||
playmatPixmap = fullRes;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::clearPlaymat()
|
||||
{
|
||||
disconnect(playmatPixmapConnection);
|
||||
playmatAttribution.clear();
|
||||
if (hasPlaymat) {
|
||||
hasPlaymat = false;
|
||||
playmatPixmap = QPixmap();
|
||||
scaledPlaymatKey = QSize();
|
||||
emit playmatChanged(false);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::onPlaymatPixmapReady()
|
||||
{
|
||||
updatePlaymat();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "../game_scene.h"
|
||||
|
||||
#include <QGraphicsObject>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class HandZone;
|
||||
class PileZone;
|
||||
|
|
@ -126,6 +127,7 @@ signals:
|
|||
void playerCountChanged();
|
||||
void mirroredChanged(bool isMirrored);
|
||||
void cardInfoRequested(const CardRef &cardRef);
|
||||
void playmatChanged(bool hasPlaymat);
|
||||
|
||||
private:
|
||||
PlayerLogic *player;
|
||||
|
|
@ -146,9 +148,23 @@ private:
|
|||
bool mirrored;
|
||||
bool handVisible = false;
|
||||
|
||||
QPixmap playmatPixmap;
|
||||
QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size
|
||||
QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for
|
||||
PlaymatParams playmatParams;
|
||||
QString playmatAttribution;
|
||||
bool hasPlaymat = false;
|
||||
QMetaObject::Connection playmatPixmapConnection;
|
||||
|
||||
private slots:
|
||||
void updateBoundingRect();
|
||||
void rearrangeZones();
|
||||
void clearPlaymat();
|
||||
void updatePlaymat();
|
||||
void onPlaymatPixmapReady();
|
||||
|
||||
private:
|
||||
QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
||||
|
|
|
|||
|
|
@ -31,8 +31,22 @@ QRectF StackZone::boundingRect() const
|
|||
|
||||
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish stack zone from table zone (slightly darker)
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 80));
|
||||
} else {
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
}
|
||||
}
|
||||
|
||||
void StackZone::onPlaymatChanged(bool active)
|
||||
{
|
||||
playmatActive = active;
|
||||
// See TableZone::onPlaymatChanged for the rationale. Translucent overlay
|
||||
// over a dynamic playmat should not be held in the device cache.
|
||||
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||
update();
|
||||
}
|
||||
|
||||
void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ class StackZone : public SelectZone
|
|||
Q_OBJECT
|
||||
private:
|
||||
qreal zoneHeight;
|
||||
bool playmatActive = false;
|
||||
private slots:
|
||||
void updateBg();
|
||||
|
||||
public slots:
|
||||
void onPlaymatChanged(bool active);
|
||||
|
||||
public:
|
||||
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
|
||||
/** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */
|
||||
|
|
|
|||
|
|
@ -92,14 +92,19 @@ bool TableZone::isInverted() const
|
|||
|
||||
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish table zone from stack zone
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 60));
|
||||
} else {
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
paintZoneOutline(painter);
|
||||
} else {
|
||||
// inactive player gets a darker table zone with a semi transparent black mask
|
||||
// this means if the user provides a custom background it will fade
|
||||
// this means if the user provides a custom background or playmat it will fade
|
||||
painter->fillRect(boundingRect(), FADE_MASK);
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +118,17 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
|
|||
paintLandDivider(painter);
|
||||
}
|
||||
|
||||
void TableZone::onPlaymatChanged(bool active)
|
||||
{
|
||||
playmatActive = active;
|
||||
// While a playmat is shown the zone paints a translucent overlay over the
|
||||
// dynamic playmat behind it. Keep it out of the device cache so the cached
|
||||
// pixels are never stale relative to the playmat (and to avoid compositing
|
||||
// artifacts of cached translucent content on some platforms).
|
||||
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
Render a soft outline around the edge of the TableZone.
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ private:
|
|||
*/
|
||||
bool active = false;
|
||||
bool mirrored = false;
|
||||
bool playmatActive = false;
|
||||
|
||||
[[nodiscard]] bool isInverted() const;
|
||||
|
||||
|
|
@ -95,6 +96,9 @@ private slots:
|
|||
*/
|
||||
void updateBg();
|
||||
|
||||
public slots:
|
||||
void onPlaymatChanged(bool active);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
Reorganizes CardItems in the TableZone
|
||||
|
|
@ -184,8 +188,17 @@ public:
|
|||
}
|
||||
void setWidth(qreal _width)
|
||||
{
|
||||
// The width is stored as an int; truncate to match the previous implicit conversion.
|
||||
const int newWidth = static_cast<int>(_width);
|
||||
if (width == newWidth) {
|
||||
return;
|
||||
}
|
||||
prepareGeometryChange();
|
||||
width = _width;
|
||||
width = newWidth;
|
||||
// The parent player item's boundingRect (which clips the playmat painting) is
|
||||
// derived from this zone's size. Without this signal the playmat is cut off at
|
||||
// the stale boundingRect edge whenever the scene is resized wider.
|
||||
emit sizeChanged();
|
||||
}
|
||||
[[nodiscard]] qreal getWidth() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../playmat/playmat_settings_dialog.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
|
@ -11,10 +12,12 @@
|
|||
#include <QDockWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
|
|
@ -228,10 +231,18 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
upperLayout->addWidget(bannerCardLabel, 4, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 4, 1);
|
||||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
|
||||
playmatLabel = new QLabel();
|
||||
playmatLabel->setObjectName("playmatLabel");
|
||||
playmatLabel->setText(tr("Playmat"));
|
||||
playmatSettingsButton = new QPushButton(tr("Edit Playmat..."));
|
||||
connect(playmatSettingsButton, &QPushButton::clicked, this, &DeckEditorDeckDockWidget::openPlaymatSettings);
|
||||
upperLayout->addWidget(playmatLabel, 5, 0);
|
||||
upperLayout->addWidget(playmatSettingsButton, 5, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 6, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 7, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 7, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
|
|
@ -440,6 +451,35 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index)
|
|||
deckStateManager->setBannerCard(bannerCard);
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::openPlaymatSettings()
|
||||
{
|
||||
PlaymatInfo current = deckStateManager->getMetadata().playmat;
|
||||
|
||||
PlaymatSettingsDialog dialog(current.card, current.params, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
CardRef newCard = dialog.card();
|
||||
PlaymatParams newParams = dialog.params();
|
||||
|
||||
if (newCard.isEmpty()) {
|
||||
deckStateManager->setPlaymat(PlaymatInfo{});
|
||||
} else {
|
||||
deckStateManager->setPlaymat({newCard, newParams});
|
||||
}
|
||||
|
||||
updatePlaymatLabel();
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::updatePlaymatLabel()
|
||||
{
|
||||
CardRef playmat = deckStateManager->getMetadata().playmat.card;
|
||||
if (playmat.isEmpty()) {
|
||||
playmatSettingsButton->setText(tr("Edit Playmat..."));
|
||||
} else {
|
||||
playmatSettingsButton->setText(tr("Edit Playmat (%1)").arg(playmat.name));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::applyActiveGroupCriteria()
|
||||
{
|
||||
getModel()->setActiveGroupCriteria(
|
||||
|
|
@ -497,6 +537,7 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
|
|||
syncBannerCardComboBoxSelectionWithDeck();
|
||||
updateBannerCardComboBox();
|
||||
bannerCardComboBox->blockSignals(false);
|
||||
updatePlaymatLabel();
|
||||
updateHash();
|
||||
|
||||
formatComboBox->blockSignals(true);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class CommanderBracketWidget;
|
||||
class DeckListModel;
|
||||
|
|
@ -33,6 +34,8 @@ public:
|
|||
DeckListStyleProxy *proxy;
|
||||
QTreeView *deckView;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QLabel *playmatLabel;
|
||||
QPushButton *playmatSettingsButton;
|
||||
void createDeckDock();
|
||||
ExactCard getCurrentCard();
|
||||
void retranslateUi();
|
||||
|
|
@ -102,6 +105,8 @@ private slots:
|
|||
void writeName();
|
||||
void writeComments();
|
||||
void writeBannerCard(int);
|
||||
void openPlaymatSettings();
|
||||
void updatePlaymatLabel();
|
||||
void applyActiveGroupCriteria();
|
||||
void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus);
|
||||
void updateHash();
|
||||
|
|
|
|||
|
|
@ -142,6 +142,19 @@ void DeckStateManager::setBannerCard(const CardRef &bannerCard)
|
|||
doMetadataModified();
|
||||
}
|
||||
|
||||
void DeckStateManager::setPlaymat(const PlaymatInfo &playmat)
|
||||
{
|
||||
PlaymatInfo previous = deckList->getPlaymat();
|
||||
if (previous == playmat) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestHistorySave(tr("Set playmat to %1").arg(playmat.card.name));
|
||||
deckList->setPlaymat(playmat);
|
||||
|
||||
doMetadataModified();
|
||||
}
|
||||
|
||||
void DeckStateManager::setTags(const QStringList &tags)
|
||||
{
|
||||
QStringList previous = deckList->getTags();
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public:
|
|||
void setName(const QString &name);
|
||||
void setComments(const QString &comments);
|
||||
void setBannerCard(const CardRef &bannerCard);
|
||||
void setPlaymat(const PlaymatInfo &playmat);
|
||||
void setTags(const QStringList &tags);
|
||||
void setFormat(const QString &format);
|
||||
///@}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
#include "playmat_collection_dialog.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "playmat_settings_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
PlaymatCollectionDialog::PlaymatCollectionDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setMinimumWidth(420);
|
||||
setupUi();
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::accept()
|
||||
{
|
||||
auto &interfaceSettings = SettingsCache::instance().userInterface();
|
||||
interfaceSettings.setPlaymatFallbackList(playmats);
|
||||
interfaceSettings.setPlaymatFallbackBehavior(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 PlaymatInfo &entry : playmats) {
|
||||
playmatList->addItem(entry.card.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(), PlaymatFallbackModeFixed);
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeRoundRobin);
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeRandom);
|
||||
const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackBehavior());
|
||||
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::addPlaymat()
|
||||
{
|
||||
PlaymatSettingsDialog dialog(CardRef{}, PlaymatParams{}, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
const CardRef card = dialog.card();
|
||||
if (!card.isEmpty()) {
|
||||
PlaymatInfo res = {card, dialog.params()};
|
||||
playmats.append(res);
|
||||
playmatList->addItem(res.card.name);
|
||||
playmatList->setCurrentRow(playmatList->count() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::editPlaymat()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PlaymatInfo ¤t = 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] = {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("List mode:"));
|
||||
modeCombo->setItemText(0, tr("Fixed (always the first entry)"));
|
||||
modeCombo->setItemText(1, tr("Round-robin (cycle through entries)"));
|
||||
modeCombo->setItemText(2, tr("Random (pick one per game)"));
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
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;
|
||||
|
||||
QList<PlaymatInfo> 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
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "playmat_utils.h"
|
||||
|
||||
#include <QLinearGradient>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
PlaymatPreviewWidget::PlaymatPreviewWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setMinimumSize(400, 120);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setPixmap(const QPixmap &pixmap)
|
||||
{
|
||||
sourcePixmap = pixmap;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setParams(const PlaymatParams &p)
|
||||
{
|
||||
params = p;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setAttribution(const QString &attribution)
|
||||
{
|
||||
attributionText = attribution;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||
|
||||
const QRect rect = this->rect();
|
||||
const QColor accentColor(100, 116, 139);
|
||||
|
||||
// Background
|
||||
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
|
||||
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
||||
bg.setColorAt(0, accentColor.darker(320));
|
||||
bg.setColorAt(1, QColor(18, 22, 30));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(bg);
|
||||
painter.drawRoundedRect(cardRect, 6, 6);
|
||||
painter.setBrush(accentColor);
|
||||
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||
|
||||
if (sourcePixmap.isNull()) {
|
||||
painter.setPen(QColor(150, 150, 150));
|
||||
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the playmat art using the same logic as PlayerGraphicsItem
|
||||
// The preview area represents the combined stack+table play area
|
||||
// Stack is ~20% width on the left, table is ~80% on the right
|
||||
const QRectF playArea = cardRect.adjusted(6, 4, -4, -4);
|
||||
|
||||
const QRectF srcRect = computeArtSourceRect(sourcePixmap.size(), params);
|
||||
const QRectF dstRect = coverFitRect(playArea, srcRect.size());
|
||||
|
||||
painter.setClipRect(playArea.toRect());
|
||||
painter.drawPixmap(dstRect, sourcePixmap, srcRect);
|
||||
painter.setClipping(false);
|
||||
|
||||
// Draw zone divider: stack is roughly the left portion
|
||||
const double stackWidthRatio = 0.18; // Stack is about 18% of total play area
|
||||
const double stackDividerX = playArea.left() + playArea.width() * stackWidthRatio;
|
||||
|
||||
// Subtle semi-transparent overlays to distinguish zones
|
||||
// Stack zone overlay (slightly darker)
|
||||
QRectF stackOverlay(playArea.left(), playArea.top(), playArea.width() * stackWidthRatio, playArea.height());
|
||||
painter.fillRect(stackOverlay, QColor(0, 0, 0, 40));
|
||||
|
||||
// Table zone overlay (very subtle)
|
||||
QRectF tableOverlay(stackDividerX, playArea.top(), playArea.width() * (1.0 - stackWidthRatio), playArea.height());
|
||||
painter.fillRect(tableOverlay, QColor(0, 0, 0, 20));
|
||||
|
||||
// Zone divider line
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 50), 1));
|
||||
painter.drawLine(QPointF(stackDividerX, playArea.top()), QPointF(stackDividerX, playArea.bottom()));
|
||||
|
||||
// Land divider line (about 60% down the table area)
|
||||
const double landDividerY = playArea.top() + playArea.height() * 0.65;
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 30), 1));
|
||||
painter.drawLine(QPointF(stackDividerX, landDividerY), QPointF(playArea.right(), landDividerY));
|
||||
|
||||
// Border around entire play area
|
||||
painter.setPen(QPen(QColor(70, 80, 95, 120), 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(playArea.adjusted(0, 0, -1, -1), 3, 3);
|
||||
|
||||
paintArtAttribution(painter, playArea, attributionText, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
#define COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
/**
|
||||
* @brief Preview widget that shows how a playmat card art will appear
|
||||
* across the combined table + stack play area.
|
||||
*
|
||||
* Renders a miniature mockup with the card art applied using the
|
||||
* given PlaymatParams, including faint zone divider lines.
|
||||
*/
|
||||
class PlaymatPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatPreviewWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
void setParams(const PlaymatParams ¶ms);
|
||||
void setAttribution(const QString &attribution);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
QPixmap sourcePixmap;
|
||||
PlaymatParams params;
|
||||
QString attributionText;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
#include "playmat_settings_dialog.h"
|
||||
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "../utility/completer_utils.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
||||
PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard,
|
||||
const PlaymatParams &initialParams,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), currentCard(initialCard), currentParams(initialParams)
|
||||
{
|
||||
setMinimumWidth(500);
|
||||
setupUi();
|
||||
|
||||
// Seed UI from initial values
|
||||
if (!initialCard.name.isEmpty()) {
|
||||
searchBar->setText(initialCard.name);
|
||||
onCardNameChanged(initialCard.name);
|
||||
|
||||
// onCardNameChanged leaves the printing combo on the first printing in
|
||||
// the database, which would silently change the deck's stored playmat
|
||||
// card on accept. Restore the stored printing when it resolves locally.
|
||||
const int storedPrintingIndex = providerComboBox->findData(initialCard.providerId);
|
||||
if (storedPrintingIndex != -1) {
|
||||
providerComboBox->setCurrentIndex(storedPrintingIndex);
|
||||
} else {
|
||||
// Stored printing not in the local database: keep it rather than
|
||||
// silently substituting the first printing.
|
||||
currentCard.providerId = initialCard.providerId;
|
||||
reloadPreview();
|
||||
}
|
||||
}
|
||||
marginLSpin->setValue(initialParams.marginPctL);
|
||||
marginRSpin->setValue(initialParams.marginPctR);
|
||||
verticalOffsetSpin->setValue(initialParams.verticalOffset);
|
||||
zoomSpin->setValue(initialParams.zoom);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
CardRef PlaymatSettingsDialog::card() const
|
||||
{
|
||||
return currentCard;
|
||||
}
|
||||
|
||||
PlaymatParams PlaymatSettingsDialog::params() const
|
||||
{
|
||||
return currentParams;
|
||||
}
|
||||
|
||||
QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, double value, double step)
|
||||
{
|
||||
auto *spin = new QDoubleSpinBox;
|
||||
spin->setRange(min, max);
|
||||
spin->setSingleStep(step);
|
||||
spin->setDecimals(3);
|
||||
spin->setValue(value);
|
||||
return spin;
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::initializeSearchBar()
|
||||
{
|
||||
searchBar = new QLineEdit;
|
||||
|
||||
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
|
||||
searchModel = cardSetup.searchModel;
|
||||
proxyModel = cardSetup.proxyModel;
|
||||
completer = cardSetup.completer;
|
||||
searchBar->setCompleter(completer);
|
||||
|
||||
connectCardCompleterSearch(searchBar, cardSetup);
|
||||
|
||||
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||
[this](const QString &completion) {
|
||||
if (searchBar->text() != completion) {
|
||||
searchBar->setText(completion);
|
||||
searchBar->setCursorPosition(searchBar->text().length());
|
||||
}
|
||||
onCardNameChanged(completion);
|
||||
});
|
||||
|
||||
connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); });
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::setupUi()
|
||||
{
|
||||
initializeSearchBar();
|
||||
|
||||
providerComboBox = new QComboBox;
|
||||
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
|
||||
currentCard.providerId = providerComboBox->currentData().toString();
|
||||
reloadPreview();
|
||||
onParamChanged();
|
||||
});
|
||||
|
||||
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
|
||||
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
|
||||
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
|
||||
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
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);
|
||||
|
||||
controlsGroup = new QGroupBox;
|
||||
controlsGroup->setLayout(form);
|
||||
|
||||
preview = new PlaymatPreviewWidget;
|
||||
|
||||
auto *previewLayout = new QVBoxLayout;
|
||||
previewLayout->addWidget(preview);
|
||||
previewGroup = new QGroupBox;
|
||||
previewGroup->setLayout(previewLayout);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
removeButton = new QPushButton;
|
||||
buttons->addButton(removeButton, QDialogButtonBox::ResetRole);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(removeButton, &QPushButton::clicked, this, [this]() {
|
||||
currentCard = CardRef{}; // empty signals removal
|
||||
accept();
|
||||
});
|
||||
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(previewGroup);
|
||||
root->addWidget(buttons);
|
||||
setLayout(root);
|
||||
|
||||
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::populateProviderCombo(const QString &cardName)
|
||||
{
|
||||
providerComboBox->clear();
|
||||
|
||||
auto card = CardDatabaseManager::query()->getCard({cardName});
|
||||
|
||||
const auto &sets = card.getInfo().getSets();
|
||||
|
||||
for (const auto &printings : sets) {
|
||||
for (const auto &p : printings) {
|
||||
QString setName = p.getSet()->getLongName();
|
||||
QString collector = p.getProperty("num");
|
||||
QString uuid = p.getUuid();
|
||||
|
||||
QString label = setName;
|
||||
if (!collector.isEmpty()) {
|
||||
label += " #" + collector;
|
||||
}
|
||||
|
||||
providerComboBox->addItem(label, uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::onCardNameChanged(const QString &name)
|
||||
{
|
||||
if (name.isEmpty()) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
const ExactCard card = CardDatabaseManager::query()->getCard({name});
|
||||
if (!card) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
providerComboBox->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
currentCard.name = name;
|
||||
|
||||
populateProviderCombo(name);
|
||||
|
||||
if (providerComboBox->count() == 0) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
currentCard.providerId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
currentCard.providerId = providerComboBox->currentData().toString();
|
||||
reloadPreview();
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::reloadPreview()
|
||||
{
|
||||
if (currentCard.name.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExactCard card = CardDatabaseManager::query()->getCard({currentCard.name, currentCard.providerId});
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnect(pixmapUpdatedConnection);
|
||||
|
||||
QPixmap fullRes;
|
||||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (fullRes.isNull()) {
|
||||
CardInfo *cardInfo = card.getCardPtr().data();
|
||||
if (cardInfo) {
|
||||
pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
currentPixmap = fullRes;
|
||||
preview->setPixmap(currentPixmap);
|
||||
preview->setParams(currentParams);
|
||||
preview->setAttribution(buildArtAttribution(card));
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::onParamChanged()
|
||||
{
|
||||
currentParams.marginPctL = marginLSpin->value();
|
||||
currentParams.marginPctR = marginRSpin->value();
|
||||
currentParams.verticalOffset = verticalOffsetSpin->value();
|
||||
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"));
|
||||
previewGroup->setTitle(tr("Preview"));
|
||||
removeButton->setText(tr("Remove Playmat"));
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
#define COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPixmap>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class QComboBox;
|
||||
class QCompleter;
|
||||
class QDoubleSpinBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class CardDatabaseModel;
|
||||
class CardDatabaseDisplayModel;
|
||||
class CardSearchModel;
|
||||
class CardCompleterProxyModel;
|
||||
class PlaymatPreviewWidget;
|
||||
|
||||
/**
|
||||
* @brief Dialog for configuring the playmat card art for a deck.
|
||||
*
|
||||
* Allows the user to select a card from the database and adjust
|
||||
* positioning parameters (margins, zoom, vertical offset) for how
|
||||
* the card art appears as a playmat background across the
|
||||
* combined table + stack play area.
|
||||
*/
|
||||
class PlaymatSettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatSettingsDialog(const CardRef &initialCard = {},
|
||||
const PlaymatParams &initialParams = {},
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
CardRef card() const;
|
||||
PlaymatParams params() const;
|
||||
|
||||
private slots:
|
||||
void onCardNameChanged(const QString &name);
|
||||
void reloadPreview();
|
||||
void onParamChanged();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateProviderCombo(const QString &cardName);
|
||||
void initializeSearchBar();
|
||||
void retranslateUi();
|
||||
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
|
||||
|
||||
QLineEdit *searchBar;
|
||||
QCompleter *completer;
|
||||
CardDatabaseModel *cardDatabaseModel;
|
||||
CardDatabaseDisplayModel *cardDatabaseDisplayModel;
|
||||
CardSearchModel *searchModel;
|
||||
CardCompleterProxyModel *proxyModel;
|
||||
|
||||
QComboBox *providerComboBox;
|
||||
|
||||
QMetaObject::Connection pixmapUpdatedConnection;
|
||||
|
||||
QLabel *cardNameLabel;
|
||||
QLabel *printingLabel;
|
||||
QLabel *leftMarginLabel;
|
||||
QLabel *rightMarginLabel;
|
||||
QLabel *verticalOffsetLabel;
|
||||
QLabel *zoomLabel;
|
||||
QGroupBox *controlsGroup;
|
||||
QGroupBox *previewGroup;
|
||||
QPushButton *removeButton;
|
||||
|
||||
QDoubleSpinBox *marginLSpin;
|
||||
QDoubleSpinBox *marginRSpin;
|
||||
QDoubleSpinBox *verticalOffsetSpin;
|
||||
QDoubleSpinBox *zoomSpin;
|
||||
PlaymatPreviewWidget *preview;
|
||||
|
||||
QPixmap currentPixmap;
|
||||
CardRef currentCard;
|
||||
PlaymatParams currentParams;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
69
cockatrice/src/interface/widgets/playmat/playmat_utils.h
Normal file
69
cockatrice/src/interface/widgets/playmat/playmat_utils.h
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_UTILS_H
|
||||
#define COCKATRICE_PLAYMAT_UTILS_H
|
||||
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QSizeF>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
/**
|
||||
* @brief Computes the source region of the full-resolution card image to use as a playmat.
|
||||
*
|
||||
* Parameters are relative to the full card image: horizontal margins trim the card
|
||||
* borders, the vertical offset positions a square viewing window, and zoom scales
|
||||
* into that window. The result is clamped to the card image bounds.
|
||||
*
|
||||
* @param fullCardSize Size of the full card image.
|
||||
* @param params Positioning parameters.
|
||||
* @return Source rectangle in full-card image pixel coordinates.
|
||||
*/
|
||||
inline QRectF computeArtSourceRect(const QSize &fullCardSize, const PlaymatParams ¶ms)
|
||||
{
|
||||
const qreal srcW = fullCardSize.width();
|
||||
const qreal srcH = fullCardSize.height();
|
||||
|
||||
const qreal marginL = params.marginPctL * srcW;
|
||||
const qreal marginR = params.marginPctR * srcW;
|
||||
// Guard against margins summing to >= 1 (both are individually in range),
|
||||
// which would otherwise make the viewing window negative or zero.
|
||||
const qreal visibleW = qMax(0.0, srcW - marginL - marginR);
|
||||
const qreal visibleH = visibleW; // square viewing window, keeps art unskewed
|
||||
|
||||
const qreal vCenter = params.verticalOffset * srcH;
|
||||
qreal srcY = vCenter - visibleH / 2.0;
|
||||
srcY = qBound(0.0, srcY, srcH - visibleH);
|
||||
|
||||
// Guard the zoom divisor; everything that produces params clamps zoom to
|
||||
// [0.1, 4.0] already, this keeps the render path self-contained.
|
||||
const qreal zoom = qBound(0.1, params.zoom, 4.0);
|
||||
const qreal zoomedW = visibleW / zoom;
|
||||
const qreal zoomedH = visibleH / zoom;
|
||||
const qreal zoomedX = marginL + (visibleW - zoomedW) / 2.0;
|
||||
const qreal zoomedY = srcY + (visibleH - zoomedH) / 2.0;
|
||||
|
||||
return QRectF(zoomedX, zoomedY, zoomedW, zoomedH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the destination rectangle that fits a source of the given aspect
|
||||
* ratio into dstArea using "cover" semantics (no distortion, overflows cropped).
|
||||
*
|
||||
* @param dstArea Area to fill.
|
||||
* @param srcSize Size of the source; only its aspect ratio matters.
|
||||
* @return Destination rectangle centered in dstArea.
|
||||
*/
|
||||
inline QRectF coverFitRect(const QRectF &dstArea, const QSizeF &srcSize)
|
||||
{
|
||||
const qreal srcAspect = srcSize.width() / srcSize.height();
|
||||
const qreal dstAspect = dstArea.width() / dstArea.height();
|
||||
|
||||
if (srcAspect > dstAspect) {
|
||||
const qreal dstW = dstArea.height() * srcAspect;
|
||||
return QRectF(dstArea.left() + (dstArea.width() - dstW) / 2.0, dstArea.top(), dstW, dstArea.height());
|
||||
}
|
||||
|
||||
const qreal dstH = dstArea.width() / srcAspect;
|
||||
return QRectF(dstArea.left(), dstArea.top() + (dstArea.height() - dstH) / 2.0, dstArea.width(), dstH);
|
||||
}
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_UTILS_H
|
||||
|
|
@ -7,11 +7,14 @@
|
|||
#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 <QApplication>
|
||||
#include <QColorDialog>
|
||||
#include <QDesktopServices>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QStyleFactory>
|
||||
#include <QTimer>
|
||||
|
|
@ -325,10 +328,52 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
tableGroupBox = new QGroupBox;
|
||||
tableGroupBox->setLayout(tableGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// putting it all together
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(themeGroupBox);
|
||||
mainLayout->addWidget(homeTabGroupBox);
|
||||
mainLayout->addWidget(playmatGroupBox);
|
||||
mainLayout->addWidget(stylingGroupBox);
|
||||
mainLayout->addWidget(menuGroupBox);
|
||||
mainLayout->addWidget(printingsGroupBox);
|
||||
|
|
@ -431,6 +476,12 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value)
|
|||
}
|
||||
}
|
||||
|
||||
void AppearanceSettingsPage::openPlaymatCollectionDialog()
|
||||
{
|
||||
PlaymatCollectionDialog dialog(this);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void AppearanceSettingsPage::retranslateUi()
|
||||
{
|
||||
themeGroupBox->setTitle(tr("Theme settings"));
|
||||
|
|
@ -489,4 +540,9 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
tableGroupBox->setTitle(tr("Table grid layout"));
|
||||
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
||||
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
|
||||
}
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ private slots:
|
|||
|
||||
void cardViewInitialRowsMaxChanged(int value);
|
||||
void cardViewExpandedRowsMaxChanged(int value);
|
||||
void openPlaymatCollectionDialog();
|
||||
|
||||
private:
|
||||
QLabel themeLabel;
|
||||
|
|
@ -59,6 +60,12 @@ private:
|
|||
QCheckBox horizontalHandCheckBox;
|
||||
QCheckBox leftJustifiedHandCheckBox;
|
||||
QCheckBox invertVerticalCoordinateCheckBox;
|
||||
QLabel playmatVisibilityLabel;
|
||||
QComboBox playmatVisibilityCombo;
|
||||
QLabel playmatModeLabel;
|
||||
QComboBox playmatModeCombo;
|
||||
QLabel playmatDefaultLabel;
|
||||
QPushButton playmatDefaultEditButton;
|
||||
QGroupBox *themeGroupBox;
|
||||
QGroupBox *homeTabGroupBox;
|
||||
QGroupBox *stylingGroupBox;
|
||||
|
|
@ -67,6 +74,7 @@ private:
|
|||
QGroupBox *cardsGroupBox;
|
||||
QGroupBox *cardLayoutGroupBox;
|
||||
QGroupBox *handGroupBox;
|
||||
QGroupBox *playmatGroupBox;
|
||||
QGroupBox *tableGroupBox;
|
||||
QGroupBox *cardCountersGroupBox;
|
||||
QList<QLabel *> cardCounterNames;
|
||||
|
|
|
|||
|
|
@ -911,6 +911,7 @@ void TabGame::stopGame()
|
|||
QMapIterator<int, TabbedDeckViewContainer *> i(deckViewContainers);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
i.value()->playerDeckView->advancePlaymatRotation();
|
||||
i.value()->show();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
@ -33,4 +35,7 @@ add_dependencies(libcockatrice_deck_list libcockatrice_protocol)
|
|||
|
||||
target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE})
|
||||
target_link_libraries(
|
||||
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
||||
${QT_CORE_MODULE}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
|
|||
|
||||
bool DeckList::Metadata::isEmpty() const
|
||||
{
|
||||
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty();
|
||||
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty();
|
||||
}
|
||||
|
||||
DeckList::DeckList()
|
||||
|
|
@ -74,6 +74,36 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
|||
QString providerId = xml->attributes().value("providerId").toString();
|
||||
QString cardName = xml->readElementText();
|
||||
metadata.bannerCard = {cardName, providerId};
|
||||
} else if (childName == "playmatCard") {
|
||||
QString providerId = xml->attributes().value("providerId").toString();
|
||||
bool ok;
|
||||
QString marginLStr = xml->attributes().value("marginPctL").toString();
|
||||
QString marginRStr = xml->attributes().value("marginPctR").toString();
|
||||
QString vOffStr = xml->attributes().value("verticalOffset").toString();
|
||||
QString zoomStr = xml->attributes().value("zoom").toString();
|
||||
QString cardName = xml->readElementText();
|
||||
PlaymatInfo playmat;
|
||||
playmat.card = {cardName, providerId};
|
||||
// Clamp to the same ranges as the settings dialog and the remote
|
||||
// player-properties path so malformed deck files cannot produce
|
||||
// degenerate art rectangles (e.g. a zoom of 0 dividing by zero).
|
||||
playmat.params.marginPctL = qBound(0.0, marginLStr.toDouble(&ok), 0.95);
|
||||
if (!ok) {
|
||||
playmat.params.marginPctL = 0.07;
|
||||
}
|
||||
playmat.params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95);
|
||||
if (!ok) {
|
||||
playmat.params.marginPctR = 0.07;
|
||||
}
|
||||
playmat.params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0);
|
||||
if (!ok) {
|
||||
playmat.params.verticalOffset = 0.33;
|
||||
}
|
||||
playmat.params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0);
|
||||
if (!ok) {
|
||||
playmat.params.zoom = 1.0;
|
||||
}
|
||||
metadata.playmat = playmat;
|
||||
} else if (childName == "tags") {
|
||||
metadata.tags.clear(); // Clear existing tags
|
||||
while (xml->readNextStartElement()) {
|
||||
|
|
@ -104,6 +134,16 @@ static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metad
|
|||
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
|
||||
xml->writeCharacters(metadata.bannerCard.name);
|
||||
xml->writeEndElement();
|
||||
if (!metadata.playmat.card.isEmpty()) {
|
||||
xml->writeStartElement("playmatCard");
|
||||
xml->writeAttribute("providerId", metadata.playmat.card.providerId);
|
||||
xml->writeAttribute("marginPctL", QString::number(metadata.playmat.params.marginPctL, 'f', 4));
|
||||
xml->writeAttribute("marginPctR", QString::number(metadata.playmat.params.marginPctR, 'f', 4));
|
||||
xml->writeAttribute("verticalOffset", QString::number(metadata.playmat.params.verticalOffset, 'f', 4));
|
||||
xml->writeAttribute("zoom", QString::number(metadata.playmat.params.zoom, 'f', 4));
|
||||
xml->writeCharacters(metadata.playmat.card.name);
|
||||
xml->writeEndElement();
|
||||
}
|
||||
xml->writeTextElement("comments", metadata.comments);
|
||||
|
||||
// Write tags
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
#include <QMap>
|
||||
#include <QVector>
|
||||
#include <QtCore/QXmlStreamReader>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
class AbstractDecklistNode;
|
||||
class DecklistCardNode;
|
||||
|
|
@ -70,6 +70,7 @@ public:
|
|||
CardRef bannerCard; ///< Optional representative card for the deck.
|
||||
QStringList tags; ///< User-defined tags for deck classification.
|
||||
QString lastLoadedTimestamp; ///< Timestamp string of last load.
|
||||
PlaymatInfo playmat; ///< Optional playmat background for table+stack zones.
|
||||
|
||||
/**
|
||||
* @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty.
|
||||
|
|
@ -115,6 +116,10 @@ public:
|
|||
{
|
||||
metadata.bannerCard = _bannerCard;
|
||||
}
|
||||
void setPlaymat(const PlaymatInfo &_playmat = {})
|
||||
{
|
||||
metadata.playmat = _playmat;
|
||||
}
|
||||
void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString())
|
||||
{
|
||||
metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
|
||||
|
|
@ -170,6 +175,10 @@ public:
|
|||
{
|
||||
return metadata.bannerCard;
|
||||
}
|
||||
PlaymatInfo getPlaymat() const
|
||||
{
|
||||
return metadata.playmat;
|
||||
}
|
||||
QString getLastLoadedTimestamp() const
|
||||
{
|
||||
return metadata.lastLoadedTimestamp;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
#include "playmat_resolver.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
|
||||
PlaymatInfo resolveEffectivePlaymat(const DeckList &deck,
|
||||
const PlaymatInfo &force,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatFallbackMode fallbackMode,
|
||||
int rotationIndex)
|
||||
{
|
||||
if (!force.card.isEmpty()) {
|
||||
return force;
|
||||
}
|
||||
|
||||
const PlaymatInfo &deckPlaymat = deck.getPlaymat();
|
||||
if (!deckPlaymat.card.isEmpty()) {
|
||||
return deckPlaymat;
|
||||
}
|
||||
|
||||
if (fallbackList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (fallbackMode) {
|
||||
case PlaymatFallbackModeFixed:
|
||||
return fallbackList.first();
|
||||
case PlaymatFallbackModeRoundRobin:
|
||||
return fallbackList.at(rotationIndex % fallbackList.size());
|
||||
case PlaymatFallbackModeRandom:
|
||||
return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size()));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
PlaymatInfo resolvePlaymatForDeck(const DeckList &deck,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatMode mode,
|
||||
PlaymatFallbackMode fallbackBehavior,
|
||||
int rotationIndex)
|
||||
{
|
||||
switch (mode) {
|
||||
case PlaymatModeOverrideDeck: {
|
||||
const DeckList emptyDeck;
|
||||
return resolveEffectivePlaymat(emptyDeck, {}, fallbackList, fallbackBehavior, rotationIndex);
|
||||
}
|
||||
case PlaymatModeFallback:
|
||||
return resolveEffectivePlaymat(deck, {}, fallbackList, fallbackBehavior, rotationIndex);
|
||||
case PlaymatModeDeckOnly:
|
||||
return deck.getPlaymat();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_RESOLVER_H
|
||||
#define COCKATRICE_PLAYMAT_RESOLVER_H
|
||||
|
||||
#include "deck_list.h"
|
||||
|
||||
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
|
||||
|
||||
/**
|
||||
* @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 PlaymatInfo::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 PlaymatFallbackModeRoundRobin;
|
||||
* advanced once per call. Unused for the other modes.
|
||||
* @return The effective playmat; an empty @ref PlaymatInfo::card when
|
||||
* nothing in the chain resolves.
|
||||
*/
|
||||
PlaymatInfo resolveEffectivePlaymat(const DeckList &deck,
|
||||
const PlaymatInfo &force,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatFallbackMode fallbackMode,
|
||||
int rotationIndex);
|
||||
|
||||
/**
|
||||
* @brief Resolves the playmat to display for a deck according to the user's
|
||||
* collection mode (@ref PlaymatMode), combining the deck with the
|
||||
* given fallback list.
|
||||
*
|
||||
* @param deck The deck to resolve a playmat for.
|
||||
* @param fallbackList User-level fallback playmats.
|
||||
* @param mode How the collection interacts with the deck-configured playmat.
|
||||
* @param fallbackBehavior How @p fallbackList is picked from.
|
||||
* @param rotationIndex Cursor for @c PlaymatFallbackModeRoundRobin.
|
||||
* @return The effective playmat; an empty @ref PlaymatInfo::card when nothing resolves.
|
||||
*/
|
||||
PlaymatInfo resolvePlaymatForDeck(const DeckList &deck,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatMode mode,
|
||||
PlaymatFallbackMode fallbackBehavior,
|
||||
int rotationIndex);
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_RESOLVER_H
|
||||
|
|
@ -30,4 +30,4 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES})
|
|||
|
||||
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_interfaces PUBLIC ${QT_CORE_MODULE})
|
||||
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,40 @@
|
|||
#ifndef COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
#define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
/**
|
||||
* @brief Whether playmats are rendered in-game, and for whom.
|
||||
*/
|
||||
enum PlaymatVisibility
|
||||
{
|
||||
PlaymatVisibilityNone = 0, ///< Don't use playmats.
|
||||
PlaymatVisibilityOwnOnly = 1, ///< Show the local player's playmat only.
|
||||
PlaymatVisibilityAll = 2 ///< Show playmats for all players.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief How the user-level playmat collection interacts with the deck-configured playmat.
|
||||
*/
|
||||
enum PlaymatMode
|
||||
{
|
||||
PlaymatModeOverrideDeck = 0, ///< Always use the collection, ignoring any deck-configured playmat.
|
||||
PlaymatModeFallback = 1, ///< Prefer the deck-configured playmat; fall back to the collection when absent.
|
||||
PlaymatModeDeckOnly = 2 ///< Use only the deck-configured playmat, ignoring the collection.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief How the user-level fallback playmat list is consulted when a deck has no playmat configured.
|
||||
*/
|
||||
enum PlaymatFallbackMode
|
||||
{
|
||||
PlaymatFallbackModeFixed = 0, ///< Always use the first entry of the fallback list.
|
||||
PlaymatFallbackModeRoundRobin = 1, ///< Cycle through the list, advancing one entry per resolution.
|
||||
PlaymatFallbackModeRandom = 2 ///< Pick a random entry per resolution.
|
||||
};
|
||||
|
||||
class IInterfaceSettingsProvider
|
||||
{
|
||||
|
|
@ -43,6 +75,21 @@ public:
|
|||
[[nodiscard]] virtual bool getLifeCounterAnimationsEnabled() const = 0;
|
||||
[[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0;
|
||||
[[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0;
|
||||
|
||||
/** @brief Who gets playmats rendered: @ref PlaymatVisibility. */
|
||||
[[nodiscard]] virtual int getPlaymatVisibility() const = 0;
|
||||
|
||||
/** @brief User-level playmat collection. Used either as a forced playmat
|
||||
* (mode == @ref PlaymatModeOverrideDeck) or as a fallback when a deck has none
|
||||
* (mode == @ref PlaymatModeFallback). */
|
||||
[[nodiscard]] virtual QList<PlaymatInfo> getPlaymatFallbackList() const = 0;
|
||||
|
||||
/** @brief How the fallback list is applied: @ref PlaymatMode. */
|
||||
[[nodiscard]] virtual int getPlaymatMode() const = 0;
|
||||
|
||||
/** @brief How the fallback list is picked from when mode is @ref PlaymatModeFallback:
|
||||
* @ref PlaymatFallbackMode. */
|
||||
[[nodiscard]] virtual int getPlaymatFallbackBehavior() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
|
|
@ -144,6 +145,13 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Com
|
|||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdSetPlaymat(const Command_SetPlaymat & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
{
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
|
|
@ -525,6 +533,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
|
|||
case GameCommand::REVERSE_TURN:
|
||||
return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges);
|
||||
break;
|
||||
case GameCommand::SET_PLAYMAT:
|
||||
return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges);
|
||||
break;
|
||||
default:
|
||||
return Response::RespInvalidCommand;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class Command_SetSideboardPlan;
|
|||
class Command_DeckSelect;
|
||||
class Command_SetSideboardLock;
|
||||
class Command_ChangeZoneProperties;
|
||||
class Command_SetPlaymat;
|
||||
|
||||
class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container
|
||||
{
|
||||
|
|
@ -124,6 +125,8 @@ public:
|
|||
cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode cmdGameSay(const Command_GameSay &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
|
|
|
|||
|
|
@ -1653,5 +1653,13 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res
|
|||
result.set_ready_start(readyStart);
|
||||
if (deck) {
|
||||
result.set_deck_hash(deck->getDeckHash().toStdString());
|
||||
const auto &playmat = deck->getPlaymat();
|
||||
auto *playmatParams = result.mutable_playmat_params();
|
||||
playmatParams->set_card_name(playmat.card.name.toStdString());
|
||||
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
|
||||
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
|
||||
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
|
||||
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
|
||||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_mulligan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
|
|
@ -250,6 +251,14 @@ Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &r
|
|||
Event_PlayerPropertiesChanged event;
|
||||
event.mutable_player_properties()->set_sideboard_locked(true);
|
||||
event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString());
|
||||
const auto &playmat = deck->getPlaymat();
|
||||
auto *playmatParams = event.mutable_player_properties()->mutable_playmat_params();
|
||||
playmatParams->set_card_name(playmat.card.name.left(MAX_NAME_LENGTH).toStdString());
|
||||
playmatParams->set_card_provider_id(playmat.card.providerId.left(MAX_NAME_LENGTH).toStdString());
|
||||
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
|
||||
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
|
||||
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
|
||||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
|
||||
Context_DeckSelect context;
|
||||
|
|
@ -594,6 +603,46 @@ Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer
|
|||
return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges);
|
||||
}
|
||||
|
||||
Response::ResponseCode
|
||||
Server_Player::cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
Q_UNUSED(rc);
|
||||
|
||||
if (!deck) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
const auto &pp = cmd.playmat_params();
|
||||
const auto rawName = QString::fromStdString(pp.card_name());
|
||||
const auto rawProviderId = QString::fromStdString(pp.card_provider_id());
|
||||
if (rawName.length() > MAX_NAME_LENGTH || rawProviderId.length() > MAX_NAME_LENGTH) {
|
||||
return Response::RespInvalidData;
|
||||
}
|
||||
PlaymatInfo playmat;
|
||||
playmat.card.name = rawName;
|
||||
playmat.card.providerId = rawProviderId;
|
||||
playmat.params.marginPctL = qBound(0.0, pp.margin_pct_l(), 0.95);
|
||||
playmat.params.marginPctR = qBound(0.0, pp.margin_pct_r(), 0.95);
|
||||
playmat.params.verticalOffset = qBound(0.0, pp.vertical_offset(), 1.0);
|
||||
playmat.params.zoom = qBound(0.1, pp.zoom(), 4.0);
|
||||
deck->setPlaymat(playmat);
|
||||
|
||||
Event_PlayerPropertiesChanged event;
|
||||
auto *props = event.mutable_player_properties();
|
||||
props->set_sideboard_locked(sideboardLocked);
|
||||
props->set_deck_hash(deck->getDeckHash().toStdString());
|
||||
auto *playmatParams = props->mutable_playmat_params();
|
||||
playmatParams->set_card_name(playmat.card.name.toStdString());
|
||||
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
|
||||
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
|
||||
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
|
||||
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
|
||||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
void Server_Player::getInfo(ServerInfo_Player *info,
|
||||
Server_AbstractParticipant *recipient,
|
||||
bool omniscient,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ public:
|
|||
cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
Response::ResponseCode
|
||||
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override;
|
||||
Response::ResponseCode
|
||||
cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd,
|
||||
ResponseContainer &rc,
|
||||
GameEventStorage &ges) override;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ set(PROTO_FILES
|
|||
command_set_card_attr.proto
|
||||
command_set_card_counter.proto
|
||||
command_set_counter.proto
|
||||
command_set_playmat.proto
|
||||
command_set_sideboard_lock.proto
|
||||
command_set_sideboard_plan.proto
|
||||
command_shuffle.proto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "game_commands.proto";
|
||||
import "serverinfo_playerproperties.proto";
|
||||
message Command_SetPlaymat {
|
||||
extend GameCommand {
|
||||
optional Command_SetPlaymat ext = 1035;
|
||||
}
|
||||
optional ServerInfo_PlayerProperties.PlaymatParams playmat_params = 1;
|
||||
}
|
||||
|
|
@ -175,6 +175,11 @@ message GameCommand {
|
|||
/// Server: Server_Player::cmdReverseTurn
|
||||
/// Client: reflected via subsequent turn events
|
||||
REVERSE_TURN = 1034;
|
||||
|
||||
/// Set the player's playmat independently of the deck.
|
||||
/// Server: Server_Player::cmdSetPlaymat
|
||||
/// Client: reflected via player properties changed event
|
||||
SET_PLAYMAT = 1035;
|
||||
}
|
||||
|
||||
extensions 100 to max;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ syntax = "proto2";
|
|||
import "serverinfo_user.proto";
|
||||
|
||||
message ServerInfo_PlayerProperties {
|
||||
message PlaymatParams {
|
||||
optional string card_name = 1;
|
||||
optional string card_provider_id = 2;
|
||||
optional double margin_pct_l = 3 [default = 0.07];
|
||||
optional double margin_pct_r = 4 [default = 0.07];
|
||||
optional double vertical_offset = 5 [default = 0.33];
|
||||
optional double zoom = 6 [default = 1.0];
|
||||
}
|
||||
|
||||
optional sint32 player_id = 1;
|
||||
optional ServerInfo_User user_info = 2;
|
||||
optional bool spectator = 3;
|
||||
|
|
@ -11,4 +20,5 @@ message ServerInfo_PlayerProperties {
|
|||
optional sint32 ping_seconds = 7;
|
||||
optional bool sideboard_locked = 8;
|
||||
optional bool judge = 9;
|
||||
optional PlaymatParams playmat_params = 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,35 @@
|
|||
#include "interface_settings.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
const QChar PLAYMAT_FIELD_SEP = QChar(0x1F); ///< Separator between PlaymatInfo fields.
|
||||
|
||||
QString encodePlaymatInfo(const PlaymatInfo &res)
|
||||
{
|
||||
return res.card.name + PLAYMAT_FIELD_SEP + res.card.providerId + PLAYMAT_FIELD_SEP +
|
||||
QString::number(res.params.marginPctL, 'f', 4) + PLAYMAT_FIELD_SEP +
|
||||
QString::number(res.params.marginPctR, 'f', 4) + PLAYMAT_FIELD_SEP +
|
||||
QString::number(res.params.verticalOffset, 'f', 4) + PLAYMAT_FIELD_SEP +
|
||||
QString::number(res.params.zoom, 'f', 4);
|
||||
}
|
||||
|
||||
PlaymatInfo decodePlaymatInfo(const QString &encoded)
|
||||
{
|
||||
const QStringList fields = encoded.split(PLAYMAT_FIELD_SEP);
|
||||
if (fields.size() != 6) {
|
||||
return {};
|
||||
}
|
||||
PlaymatInfo res;
|
||||
res.card.name = fields.at(0);
|
||||
res.card.providerId = fields.at(1);
|
||||
res.params.marginPctL = fields.at(2).toDouble();
|
||||
res.params.marginPctR = fields.at(3).toDouble();
|
||||
res.params.verticalOffset = fields.at(4).toDouble();
|
||||
res.params.zoom = fields.at(5).toDouble();
|
||||
return res;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InterfaceSettings::InterfaceSettings(const QString &settingPath, QObject *parent)
|
||||
: SettingsManager(settingPath + "interface.ini", "interface", QString(), parent)
|
||||
{
|
||||
|
|
@ -160,6 +190,35 @@ bool InterfaceSettings::getShowGameSelectorFilterToolbar() const
|
|||
return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool();
|
||||
}
|
||||
|
||||
int InterfaceSettings::getPlaymatVisibility() const
|
||||
{
|
||||
return qBound(0, getValue("playmatvisibility", QString(), QString(), 2).toInt(), 2);
|
||||
}
|
||||
|
||||
QList<PlaymatInfo> InterfaceSettings::getPlaymatFallbackList() const
|
||||
{
|
||||
const QStringList entries = getValue("playmatFallbackList", QString(), QString(), QStringList()).toStringList();
|
||||
QList<PlaymatInfo> result;
|
||||
result.reserve(entries.size());
|
||||
for (const QString &entry : entries) {
|
||||
const PlaymatInfo res = decodePlaymatInfo(entry);
|
||||
if (!res.card.isEmpty()) {
|
||||
result.append(res);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int InterfaceSettings::getPlaymatMode() const
|
||||
{
|
||||
return qBound(0, getValue("playmatMode", QString(), QString(), 1).toInt(), 2);
|
||||
}
|
||||
|
||||
int InterfaceSettings::getPlaymatFallbackBehavior() const
|
||||
{
|
||||
return qBound(0, getValue("playmatFallbackBehavior", QString(), QString(), 0).toInt(), 2);
|
||||
}
|
||||
|
||||
bool InterfaceSettings::getLifeCounterAnimationsEnabled() const
|
||||
{
|
||||
return getValue("lifeCounterAnimationsEnabled", QString(), QString(), true).toBool();
|
||||
|
|
@ -343,6 +402,46 @@ void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorF
|
|||
emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar);
|
||||
}
|
||||
|
||||
void InterfaceSettings::setPlaymatVisibility(int _visibility)
|
||||
{
|
||||
if (getPlaymatVisibility() == _visibility) {
|
||||
return;
|
||||
}
|
||||
setValue(_visibility, "playmatvisibility");
|
||||
emit playmatVisibilityChanged(_visibility);
|
||||
}
|
||||
|
||||
void InterfaceSettings::setPlaymatFallbackList(const QList<PlaymatInfo> &_fallbackList)
|
||||
{
|
||||
QStringList entries;
|
||||
entries.reserve(_fallbackList.size());
|
||||
for (const PlaymatInfo &res : _fallbackList) {
|
||||
entries.append(encodePlaymatInfo(res));
|
||||
}
|
||||
setValue(entries, "playmatFallbackList");
|
||||
emit playmatSettingsChanged();
|
||||
}
|
||||
|
||||
void InterfaceSettings::setPlaymatMode(int _mode)
|
||||
{
|
||||
const int mode = qBound(0, _mode, 2);
|
||||
if (getPlaymatMode() == mode) {
|
||||
return;
|
||||
}
|
||||
setValue(mode, "playmatMode");
|
||||
emit playmatSettingsChanged();
|
||||
}
|
||||
|
||||
void InterfaceSettings::setPlaymatFallbackBehavior(int _behavior)
|
||||
{
|
||||
const int behavior = qBound(0, _behavior, 2);
|
||||
if (getPlaymatFallbackBehavior() == behavior) {
|
||||
return;
|
||||
}
|
||||
setValue(behavior, "playmatFallbackBehavior");
|
||||
emit playmatSettingsChanged();
|
||||
}
|
||||
|
||||
void InterfaceSettings::setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled)
|
||||
{
|
||||
setValue(_lifeCounterAnimationsEnabled, "lifeCounterAnimationsEnabled");
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ public:
|
|||
[[nodiscard]] bool getShowStatusBar() const override;
|
||||
[[nodiscard]] bool getShowShortcuts() const override;
|
||||
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const override;
|
||||
[[nodiscard]] int getPlaymatVisibility() const override;
|
||||
[[nodiscard]] QList<PlaymatInfo> getPlaymatFallbackList() const override;
|
||||
[[nodiscard]] int getPlaymatMode() const override;
|
||||
[[nodiscard]] int getPlaymatFallbackBehavior() const override;
|
||||
[[nodiscard]] bool getLifeCounterAnimationsEnabled() const override;
|
||||
[[nodiscard]] bool getBattlefieldFlashEnabled() const override;
|
||||
[[nodiscard]] QStringList getUserListExpandedSections() const override;
|
||||
|
|
@ -77,6 +81,10 @@ public:
|
|||
void setShowStatusBar(bool _showStatusBar);
|
||||
void setShowShortcuts(bool _showShortcuts);
|
||||
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
|
||||
void setPlaymatVisibility(int _visibility);
|
||||
void setPlaymatFallbackList(const QList<PlaymatInfo> &_fallbackList);
|
||||
void setPlaymatMode(int _mode);
|
||||
void setPlaymatFallbackBehavior(int _behavior);
|
||||
void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled);
|
||||
void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled);
|
||||
void setUserListExpandedSections(const QStringList §ions);
|
||||
|
|
@ -91,6 +99,8 @@ signals:
|
|||
void tallyTypeChanged(int type);
|
||||
void showStatusBarChanged(bool state);
|
||||
void showGameSelectorFilterToolbarChanged(bool state);
|
||||
void playmatVisibilityChanged(int visibility);
|
||||
void playmatSettingsChanged();
|
||||
void lifeCounterAnimationsEnabledChanged(bool state);
|
||||
void battlefieldFlashEnabledChanged(bool state);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/l
|
|||
)
|
||||
|
||||
set(UTILITY_HEADERS
|
||||
libcockatrice/utility/card_ref.h
|
||||
libcockatrice/utility/color.h
|
||||
libcockatrice/utility/expression.h
|
||||
libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/macros.h
|
||||
libcockatrice/utility/passwordhasher.h
|
||||
libcockatrice/utility/playmat_params.h
|
||||
libcockatrice/utility/string_limits.h
|
||||
libcockatrice/utility/dice_limits.h
|
||||
libcockatrice/utility/counter_limits.h
|
||||
|
|
|
|||
52
libcockatrice_utility/libcockatrice/utility/playmat_params.h
Normal file
52
libcockatrice_utility/libcockatrice/utility/playmat_params.h
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_PARAMS_H
|
||||
#define COCKATRICE_PLAYMAT_PARAMS_H
|
||||
|
||||
#include "card_ref.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
/**
|
||||
* @struct PlaymatParams
|
||||
* @ingroup Decks
|
||||
* @brief Positioning parameters for a playmat card image.
|
||||
*
|
||||
* Controls how the cropped card art is positioned within the
|
||||
* combined table+stack play area. The coordinate system is
|
||||
* relative to the cropped art source image.
|
||||
*/
|
||||
struct PlaymatParams
|
||||
{
|
||||
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 operator==(const PlaymatParams &other) const
|
||||
{
|
||||
return qFuzzyCompare(marginPctL, other.marginPctL) && qFuzzyCompare(marginPctR, other.marginPctR) &&
|
||||
qFuzzyCompare(verticalOffset, other.verticalOffset) && qFuzzyCompare(zoom, other.zoom);
|
||||
}
|
||||
|
||||
bool operator!=(const PlaymatParams &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct PlaymatInfo
|
||||
* @ingroup Decks
|
||||
* @brief A resolved playmat (card + positioning parameters).
|
||||
*/
|
||||
struct PlaymatInfo
|
||||
{
|
||||
CardRef card; ///< The card whose art is used as playmat.
|
||||
PlaymatParams params; ///< Positioning parameters for the playmat card image.
|
||||
|
||||
bool operator==(const PlaymatInfo &other) const
|
||||
{
|
||||
return card == other.card && params == other.params;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_PARAMS_H
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
126
tests/playmat_resolver_test.cpp
Normal file
126
tests/playmat_resolver_test.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
PlaymatInfo makePlaymatInfo(const QString &name, const QString &providerId = QString())
|
||||
{
|
||||
PlaymatInfo info;
|
||||
info.card = {name, providerId};
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(PlaymatResolverTest, EmptyChainReturnsEmpty)
|
||||
{
|
||||
DeckList deck;
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, {}, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_TRUE(resolved.card.isEmpty());
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, OverrideWinsOverDeckAndFallback)
|
||||
{
|
||||
DeckList deck;
|
||||
deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {}});
|
||||
|
||||
const PlaymatInfo force = makePlaymatInfo(QStringLiteral("Force Mat"), QStringLiteral("force-provider"));
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, force, fallback, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Force Mat"));
|
||||
EXPECT_EQ(resolved.card.providerId, QStringLiteral("force-provider"));
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, DeckWinsOverFallback)
|
||||
{
|
||||
DeckList deck;
|
||||
deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {0.1, 0.2, 0.3, 1.5}});
|
||||
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, 0);
|
||||
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<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Fallback Mat"));
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, FixedAlwaysUsesFirst)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second"))};
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, i);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("First"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, RoundRobinCyclesAndWraps)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second")),
|
||||
makePlaymatInfo(QStringLiteral("Third"))};
|
||||
|
||||
const QStringList expected = {QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third"),
|
||||
QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third")};
|
||||
for (int i = 0; i < expected.size(); ++i) {
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRoundRobin, i);
|
||||
EXPECT_EQ(resolved.card.name, expected.at(i));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, RoundRobinRespectsCursor)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRoundRobin, 5);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Second")); // 5 % 2 == 1
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, RandomStaysWithinList)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second")),
|
||||
makePlaymatInfo(QStringLiteral("Third"))};
|
||||
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRandom, i);
|
||||
ASSERT_FALSE(resolved.card.name.isEmpty());
|
||||
EXPECT_TRUE(fallback.contains(resolved));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, ForceWithEmptyCardIgnoresFallbackParamsButNotFallback)
|
||||
{
|
||||
DeckList deck;
|
||||
deck.setPlaymat({{QStringLiteral("Deck Mat")}, {}});
|
||||
|
||||
// An empty force entry must not mask the deck-configured playmat.
|
||||
const PlaymatInfo emptyForce;
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, emptyForce, {}, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat"));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue