Merge pull request #5 from Continuum-Universes-Studio/feature/continuumatrice-essence
Continuumatrice CCG: 16 Essence symbols including Jasper J and colorless N
|
|
@ -157,6 +157,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/palette_editor/palette_editor_dialog.cpp
|
||||
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
||||
src/interface/widgets/cards/additional_info/essence_filter_picker.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||
|
|
|
|||
|
|
@ -60,12 +60,22 @@
|
|||
<file>resources/icons/update.png</file>
|
||||
<file>resources/icons/view.svg</file>
|
||||
|
||||
<file>resources/icons/mana/B.svg</file>
|
||||
<file>resources/icons/mana/C.svg</file>
|
||||
<file>resources/icons/mana/G.svg</file>
|
||||
<file>resources/icons/mana/R.svg</file>
|
||||
<file>resources/icons/mana/U.svg</file>
|
||||
<file>resources/icons/mana/W.svg</file>
|
||||
<file>resources/icons/essence/M.svg</file>
|
||||
<file>resources/icons/essence/S.svg</file>
|
||||
<file>resources/icons/essence/O.svg</file>
|
||||
<file>resources/icons/essence/G.svg</file>
|
||||
<file>resources/icons/essence/E.svg</file>
|
||||
<file>resources/icons/essence/A.svg</file>
|
||||
<file>resources/icons/essence/Q.svg</file>
|
||||
<file>resources/icons/essence/C.svg</file>
|
||||
<file>resources/icons/essence/J.svg</file>
|
||||
<file>resources/icons/essence/B.svg</file>
|
||||
<file>resources/icons/essence/R.svg</file>
|
||||
<file>resources/icons/essence/L.svg</file>
|
||||
<file>resources/icons/essence/V.svg</file>
|
||||
<file>resources/icons/essence/T.svg</file>
|
||||
<file>resources/icons/essence/P.svg</file>
|
||||
<file>resources/icons/essence/N.svg</file>
|
||||
|
||||
<file>resources/backgrounds/home.jpg</file>
|
||||
<file>resources/backgrounds/home-dark.jpg</file>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 7.3 MiB |
11
cockatrice/resources/icons/essence/A.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>A Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">A</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/B.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>B Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">B</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/C.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>C Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">C</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/E.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>E Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">E</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/G.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>G Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">G</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/J.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>J Essence (Jasper)</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">J</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 662 B |
11
cockatrice/resources/icons/essence/L.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>L Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">L</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/M.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>M Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">M</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/N.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>N Essence (colorless)</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#272A30"/><stop offset="1" stop-color="#0B0D12"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#A8AFBC" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#A8AFBC" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F4F5F7">N</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 665 B |
11
cockatrice/resources/icons/essence/O.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>O Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">O</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/P.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>P Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">P</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/Q.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>Q Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">Q</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/R.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>R Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">R</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/S.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>S Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">S</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/T.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>T Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">T</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
11
cockatrice/resources/icons/essence/V.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<title>V Essence</title>
|
||||
<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#222139"/><stop offset="1" stop-color="#131423"/>
|
||||
</linearGradient></defs>
|
||||
<circle cx="48" cy="48" r="43" fill="url(#g)" stroke="#ACB5E9" stroke-width="4"/>
|
||||
<circle cx="48" cy="48" r="35" fill="none" stroke="#ACB5E9" stroke-opacity=".35" stroke-width="1.5"/>
|
||||
<text x="48" y="65" text-anchor="middle" font-family="DejaVu Sans,Liberation Sans,sans-serif"
|
||||
font-size="46" font-weight="700" fill="#F1F0FF">V</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
|
|
@ -13,6 +13,7 @@
|
|||
#include "libcockatrice/protocol/pb/event_server_shutdown.pb.h"
|
||||
|
||||
#include <QChar>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QLineEdit>
|
||||
|
|
@ -217,7 +218,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
|
|||
"games will be lost.\nReason for shutdown: %1",
|
||||
"", event.minutes())
|
||||
.arg(QString::fromStdString(event.reason())));
|
||||
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
|
||||
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("continuumatrice")).scaled(64, 64));
|
||||
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
||||
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
||||
serverShutdownMessageBox.setVisible(true);
|
||||
|
|
@ -333,7 +334,7 @@ void ConnectionController::onLoginError(int r,
|
|||
|
||||
case Response::RespContextError: {
|
||||
QMessageBox::critical(dialogParent, tr("Error"),
|
||||
tr("An internal error has occurred, please close and reopen Cockatrice before "
|
||||
tr("An internal error has occurred, please close and reopen Continuumatrice before "
|
||||
"trying again.\nIf the error persists, ensure you are running the latest "
|
||||
"version of the software and if needed contact the software developers."));
|
||||
break;
|
||||
|
|
@ -489,14 +490,14 @@ void ConnectionController::onProtocolVersionMismatch(int localVersion, int remot
|
|||
{
|
||||
if (localVersion > remoteVersion) {
|
||||
QMessageBox::critical(dialogParent, tr("Error"),
|
||||
tr("You are trying to connect to an obsolete server. Please downgrade your Cockatrice "
|
||||
tr("You are trying to connect to an obsolete server. Please downgrade your Continuumatrice "
|
||||
"version or connect to a suitable server.\n"
|
||||
"Local version is %1, remote version is %2.")
|
||||
.arg(localVersion)
|
||||
.arg(remoteVersion));
|
||||
} else {
|
||||
QMessageBox::critical(dialogParent, tr("Error"),
|
||||
tr("Your Cockatrice client is obsolete. Please update your Cockatrice version.\n"
|
||||
tr("Your Continuumatrice client is obsolete. Please update your Continuumatrice version.\n"
|
||||
"Local version is %1, remote version is %2.")
|
||||
.arg(localVersion)
|
||||
.arg(remoteVersion));
|
||||
|
|
@ -631,7 +632,7 @@ void ConnectionController::onPromptForgotPasswordChallenge()
|
|||
|
||||
void ConnectionController::updateWindowTitle()
|
||||
{
|
||||
const QString appName = QStringLiteral("Cockatrice");
|
||||
const QString appName = QCoreApplication::applicationName();
|
||||
QString title;
|
||||
|
||||
switch (remoteClient->getStatus()) {
|
||||
|
|
@ -653,7 +654,7 @@ void ConnectionController::updateWindowTitle()
|
|||
break;
|
||||
}
|
||||
case StatusLoggedIn: {
|
||||
title = remoteClient->getUserName() + "@" + remoteClient->peerName();
|
||||
title = appName + " - " + remoteClient->getUserName() + "@" + remoteClient->peerName();
|
||||
break;
|
||||
}
|
||||
case StatusRequestingForgotPassword:
|
||||
|
|
|
|||
|
|
@ -251,9 +251,11 @@ void TableZone::paintBattlefieldRows(QPainter *painter)
|
|||
*/
|
||||
void TableZone::reloadBattlefieldAppearance()
|
||||
{
|
||||
frontZoneColor = QColor(142, 52, 61, 128);
|
||||
supportZoneColor = QColor(40, 105, 129, 128);
|
||||
commandZoneColor = QColor(94, 66, 140, 128);
|
||||
// Canonical default battlefield palette. Theme battlefield.ini can override
|
||||
// these with #RRGGBB (opaque) or #AARRGGBB (alpha) values.
|
||||
frontZoneColor = QColor(QStringLiteral("#dc143c"));
|
||||
supportZoneColor = QColor(QStringLiteral("#c3b091"));
|
||||
commandZoneColor = QColor(QStringLiteral("#0a0e3f"));
|
||||
|
||||
const QString themePath = themeManager->getCurrentThemePath();
|
||||
if (!themePath.isEmpty()) {
|
||||
|
|
|
|||
55
cockatrice/src/interface/essence_chart_colors.h
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#ifndef CONTINUUMATRICE_ESSENCE_CHART_COLORS_H
|
||||
#define CONTINUUMATRICE_ESSENCE_CHART_COLORS_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
|
||||
// CHART-ONLY presentation palette, NOT canon Essence colors or affiliations.
|
||||
// Each type must remain distinguishable without inventing lore. N is colorless.
|
||||
namespace EssenceCharts
|
||||
{
|
||||
inline QColor colorFor(const QString &code)
|
||||
{
|
||||
if (code == QStringLiteral("N")) {
|
||||
return QColor(145, 149, 156);
|
||||
}
|
||||
const int index = Essence::symbols().indexOf(code);
|
||||
if (index < 0) {
|
||||
// These charts are also reused for card types/categories. Retain
|
||||
// distinct readable visualization colors without assigning Essence lore.
|
||||
const QColor recognizedColor(code);
|
||||
if (recognizedColor.isValid()) {
|
||||
return recognizedColor;
|
||||
}
|
||||
return QColor::fromHsv(static_cast<int>(qHash(code) % 360u), 110, 195);
|
||||
}
|
||||
return QColor::fromHsv((index * 137) % 360, 125, 210);
|
||||
}
|
||||
|
||||
inline QList<QPair<QString, int>> ordered(const QMap<QString, int> &values)
|
||||
{
|
||||
QList<QPair<QString, int>> result;
|
||||
QSet<QString> used;
|
||||
for (const QChar code : Essence::symbols()) {
|
||||
const QString key(code);
|
||||
auto it = values.constFind(key);
|
||||
if (it != values.cend()) {
|
||||
result.append({it.key(), it.value()});
|
||||
used.insert(key);
|
||||
}
|
||||
}
|
||||
for (auto it = values.cbegin(); it != values.cend(); ++it) {
|
||||
if (!used.contains(it.key())) {
|
||||
result.append({it.key(), it.value()});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace EssenceCharts
|
||||
|
||||
#endif // CONTINUUMATRICE_ESSENCE_CHART_COLORS_H
|
||||
|
|
@ -488,7 +488,10 @@ const QPixmap &ManaSymbolPixmapGenerator::masterIcon(const QString &symbol)
|
|||
return it.value();
|
||||
}
|
||||
|
||||
QImageReader reader("theme:icons/mana/" + symbol);
|
||||
// Essence uses its own artwork namespace, including codes shared with MTG.
|
||||
// Theme overrides are supported through icons/essence/<symbol>.svg/png.
|
||||
const QString asset = themeManager->assetPath(QStringLiteral("icons/essence/") + symbol);
|
||||
QImageReader reader(QStringLiteral("theme:") + asset);
|
||||
QSize sourceSize = reader.size();
|
||||
if (!sourceSize.isEmpty()) {
|
||||
sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include "mana_symbol_widget.h"
|
||||
|
||||
#include <QChar>
|
||||
|
|
@ -41,9 +42,9 @@ ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const CardInfoPtr &car
|
|||
|
||||
void ColorIdentityWidget::populateManaSymbolWidgets()
|
||||
{
|
||||
// Define the full WUBRG set (White, Blue, Black, Red, Green)
|
||||
QString fullColorIdentity = "WUBRG";
|
||||
QStringList symbols = parseColorIdentity(colorIdentity); // Parse mana cost string
|
||||
// Continuum Universes supports fourteen named Essence codes plus N (colorless).
|
||||
const QString &allEssences = Essence::symbols();
|
||||
QStringList symbols = parseColorIdentity(colorIdentity);
|
||||
|
||||
// clear old layout
|
||||
QtUtils::clearLayoutRec(layout);
|
||||
|
|
@ -55,7 +56,8 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
|
|||
|
||||
// populate mana symbols
|
||||
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) {
|
||||
for (const QString symbol : fullColorIdentity) {
|
||||
for (const QChar code : allEssences) {
|
||||
const QString symbol(code);
|
||||
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
|
||||
layout->addWidget(manaSymbol);
|
||||
}
|
||||
|
|
@ -120,28 +122,11 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
QStringList ColorIdentityWidget::parseColorIdentity(const QString &manaString)
|
||||
QStringList ColorIdentityWidget::parseColorIdentity(const QString &essenceIdentity)
|
||||
{
|
||||
QStringList symbols;
|
||||
|
||||
// Handle split costs (e.g., "3U // 4UU")
|
||||
QStringList splitCosts = manaString.split(" // ");
|
||||
for (const QString &part : splitCosts) {
|
||||
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
|
||||
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
|
||||
while (matches.hasNext()) {
|
||||
QRegularExpressionMatch match = matches.next();
|
||||
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
|
||||
if (!match.captured(2).isEmpty()) {
|
||||
symbols.append(match.captured(2)); // Number match
|
||||
} else {
|
||||
symbols.append(match.captured(3)); // Single mana letter match
|
||||
}
|
||||
} else {
|
||||
symbols.append(match.captured(1)); // `{}` enclosed match
|
||||
}
|
||||
}
|
||||
for (const QChar code : Essence::normalizeIdentity(essenceIdentity)) {
|
||||
symbols.append(QString(code));
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "deck_color_identity.h"
|
||||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
|
||||
#include <QChar>
|
||||
#include <QList>
|
||||
|
|
@ -17,7 +18,7 @@ QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db
|
|||
return {};
|
||||
}
|
||||
|
||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||
QSet<QChar> colorSet; // Unique Essence symbols (including N for colorless).
|
||||
|
||||
for (const QString &cardName : cardList) {
|
||||
CardInfoPtr currentCard = db->getCardInfo(cardName);
|
||||
|
|
@ -29,10 +30,9 @@ QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure the color identity is in WUBRG order
|
||||
// Canonical Essence order is shared by the database and all widgets.
|
||||
QString colorIdentity;
|
||||
const QString wubrgOrder = "WUBRG";
|
||||
for (const QChar &color : wubrgOrder) {
|
||||
for (const QChar &color : Essence::symbols()) {
|
||||
if (colorSet.contains(color)) {
|
||||
colorIdentity.append(color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ class CardDatabaseQuerier;
|
|||
class DeckList;
|
||||
|
||||
/**
|
||||
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
|
||||
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
|
||||
* @brief Computes the Essence identity of a deck from the defined Essence
|
||||
* symbols (M,S,O,G,E,A,Q,C,J,B,R,L,V,T,P,N) of cards in main/sideboard.
|
||||
* N denotes colorless.
|
||||
*
|
||||
* Shared as a free function so the deck storage previews and the deck share
|
||||
* dialog compute identities identically.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
#include "essence_filter_picker.h"
|
||||
|
||||
#include "../../../pixel_map_generator.h"
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSize>
|
||||
#include <QSizePolicy>
|
||||
#include <QStringList>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QWidgetAction>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
|
||||
EssenceFilterPicker::EssenceFilterPicker(QWidget *parent) : QToolButton(parent)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolButtonStyle(Qt::ToolButtonTextOnly); // Keep the summary visible even without a button icon.
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
// Force a stable toolbar footprint independent of popup and font sizing.
|
||||
// Modes and all 16 Essence icons are contained inside the popup.
|
||||
setFixedWidth(qMax(156, fontMetrics().horizontalAdvance(tr("Essence: Any")) + 42));
|
||||
|
||||
popupMenu = new QMenu(this);
|
||||
auto *panel = new QWidget(popupMenu);
|
||||
panelLayout = new QVBoxLayout(panel);
|
||||
panelLayout->setContentsMargins(10, 10, 10, 10);
|
||||
panelLayout->setSpacing(7);
|
||||
|
||||
auto *heading = new QLabel(tr("Filter by Essence"), panel);
|
||||
panelLayout->addWidget(heading);
|
||||
|
||||
auto *grid = new QGridLayout();
|
||||
grid->setSpacing(5);
|
||||
|
||||
int index = 0;
|
||||
for (const QChar essence : Essence::symbols()) {
|
||||
const QString symbol(essence);
|
||||
auto *button = new QToolButton(panel);
|
||||
button->setCheckable(true);
|
||||
button->setAutoRaise(false);
|
||||
button->setFixedSize(42, 42);
|
||||
button->setIcon(QIcon(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(32, 32))));
|
||||
button->setIconSize(QSize(30, 30));
|
||||
if (essence == QChar('N')) {
|
||||
button->setToolTip(tr("N — Colorless Essence"));
|
||||
button->setAccessibleName(tr("Colorless Essence N"));
|
||||
} else if (essence == QChar('J')) {
|
||||
button->setToolTip(tr("J — Jasper Essence"));
|
||||
button->setAccessibleName(tr("Jasper Essence J"));
|
||||
} else {
|
||||
button->setToolTip(tr("Essence %1").arg(symbol));
|
||||
button->setAccessibleName(tr("Essence %1").arg(symbol));
|
||||
}
|
||||
button->setProperty("essenceCode", symbol);
|
||||
connect(button, &QToolButton::toggled, this, [this, essence](bool checked) {
|
||||
updateSummary();
|
||||
emit essenceToggled(essence, checked);
|
||||
});
|
||||
|
||||
// Sixteen Essences form a compact 4x4 grid.
|
||||
grid->addWidget(button, index / 4, index % 4);
|
||||
buttons.insert(essence, button);
|
||||
++index;
|
||||
}
|
||||
panelLayout->addLayout(grid);
|
||||
|
||||
auto *clearButton = new QPushButton(tr("Clear Essence selection"), panel);
|
||||
connect(clearButton, &QPushButton::clicked, this, [this]() {
|
||||
// Clear as ONE model operation, so Exact Match and Include/Exclude
|
||||
// don't retain stale "not this Essence" predicates.
|
||||
for (const QChar essence : Essence::symbols()) {
|
||||
if (auto *button = buttons.value(essence); button && button->isChecked()) {
|
||||
const QSignalBlocker blocker(button);
|
||||
button->setChecked(false);
|
||||
}
|
||||
}
|
||||
updateSummary();
|
||||
emit selectionCleared();
|
||||
});
|
||||
panelLayout->addWidget(clearButton);
|
||||
|
||||
auto *panelAction = new QWidgetAction(popupMenu);
|
||||
panelAction->setDefaultWidget(panel);
|
||||
popupMenu->addAction(panelAction);
|
||||
setMenu(popupMenu);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void EssenceFilterPicker::addPopupControl(QWidget *control)
|
||||
{
|
||||
if (!control || !panelLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert above the Clear button, keeping the entire configuration in one
|
||||
// popup rather than competing for horizontal space in the main toolbar.
|
||||
panelLayout->insertWidget(panelLayout->count() - 1, control);
|
||||
}
|
||||
|
||||
QSet<QChar> EssenceFilterPicker::activeEssences() const
|
||||
{
|
||||
QSet<QChar> active;
|
||||
for (auto it = buttons.cbegin(); it != buttons.cend(); ++it) {
|
||||
if (it.value()->isChecked()) {
|
||||
active.insert(it.key());
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
void EssenceFilterPicker::setEssenceActive(QChar essence, bool active)
|
||||
{
|
||||
auto *button = buttons.value(essence);
|
||||
if (!button || button->isChecked() == active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restoring external filter state must not rewrite the filter model.
|
||||
const QSignalBlocker blocker(button);
|
||||
button->setChecked(active);
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
void EssenceFilterPicker::retranslateUi()
|
||||
{
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
void EssenceFilterPicker::updateSummary()
|
||||
{
|
||||
QStringList selected;
|
||||
const QSet<QChar> active = activeEssences();
|
||||
for (const QChar essence : Essence::symbols()) {
|
||||
if (active.contains(essence)) {
|
||||
selected.append(QString(essence));
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.isEmpty()) {
|
||||
setText(tr("Essence: Any"));
|
||||
setToolTip(tr("Filter by Essence (none selected)"));
|
||||
} else if (selected.size() == 1) {
|
||||
setText(tr("Essence: %1").arg(selected.first()));
|
||||
setToolTip(tr("Selected Essence: %1").arg(selected.first()));
|
||||
} else {
|
||||
// Keep the toolbar label short; the tooltip lists the exact types.
|
||||
setText(tr("Essence (%1)").arg(selected.size()));
|
||||
setToolTip(tr("Selected Essence: %1").arg(selected.join(QStringLiteral(", "))));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#ifndef CONTINUUMATRICE_ESSENCE_FILTER_PICKER_H
|
||||
#define CONTINUUMATRICE_ESSENCE_FILTER_PICKER_H
|
||||
|
||||
#include <QChar>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QToolButton>
|
||||
|
||||
class QMenu;
|
||||
class QVBoxLayout;
|
||||
class QWidget;
|
||||
|
||||
/**
|
||||
* Compact, reusable multi-select Essence picker for the deck and card database
|
||||
* toolbars. The menu contains all fifteen Essence symbols in a 5x3 icon grid.
|
||||
* Clicking a symbol does not close the popup; programmatic synchronization
|
||||
* does not emit a user-selection signal.
|
||||
*/
|
||||
class EssenceFilterPicker final : public QToolButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EssenceFilterPicker(QWidget *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QSet<QChar> activeEssences() const;
|
||||
void setEssenceActive(QChar essence, bool active);
|
||||
// Put the host's filtering mode selector inside this popup too:
|
||||
// neither toolbar should require a separate cramped control.
|
||||
void addPopupControl(QWidget *control);
|
||||
void retranslateUi();
|
||||
|
||||
signals:
|
||||
void essenceToggled(QChar essence, bool active);
|
||||
// A clear request removes ALL Essence filters, including exclusions,
|
||||
// rather than creating fifteen negative filters in Exact/Exclude mode.
|
||||
void selectionCleared();
|
||||
|
||||
private:
|
||||
void updateSummary();
|
||||
|
||||
QHash<QChar, QToolButton *> buttons;
|
||||
QMenu *popupMenu = nullptr;
|
||||
QVBoxLayout *panelLayout = nullptr;
|
||||
};
|
||||
|
||||
#endif // CONTINUUMATRICE_ESSENCE_FILTER_PICKER_H
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#include "mana_cost_widget.h"
|
||||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include "mana_symbol_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -51,28 +52,7 @@ void ManaCostWidget::resizeEvent(QResizeEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
QStringList ManaCostWidget::parseManaCost(const QString &cmc)
|
||||
QStringList ManaCostWidget::parseManaCost(const QString &essenceCost)
|
||||
{
|
||||
QStringList symbols;
|
||||
|
||||
// Handle split costs (e.g., "3U // 4UU")
|
||||
QStringList splitCosts = cmc.split(" // ");
|
||||
for (const QString &part : splitCosts) {
|
||||
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
|
||||
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
|
||||
while (matches.hasNext()) {
|
||||
QRegularExpressionMatch match = matches.next();
|
||||
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
|
||||
if (!match.captured(2).isEmpty()) {
|
||||
symbols.append(match.captured(2)); // Number match
|
||||
} else {
|
||||
symbols.append(match.captured(3)); // Single mana letter match
|
||||
}
|
||||
} else {
|
||||
symbols.append(match.captured(1)); // `{}` enclosed match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
return Essence::parseCost(essenceCost);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
|
|||
|
||||
void ManaBaseConfigDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Mana Base Configuration"));
|
||||
setWindowTitle(tr("Essence Production Configuration"));
|
||||
|
||||
displayTypeLabel->setText(tr("Display type:"));
|
||||
|
||||
displayType->clear();
|
||||
displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")});
|
||||
|
||||
filterLabel->setText(tr("Filter Colors (optional):"));
|
||||
filterLabel->setText(tr("Filter Essence types (optional):"));
|
||||
|
||||
buttons->button(QDialogButtonBox::Ok)->setText(tr("OK"));
|
||||
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "mana_base_widget.h"
|
||||
|
||||
#include "../../../../essence_chart_colors.h"
|
||||
#include "../../../general/display/charts/bars/bar_widget.h"
|
||||
#include "../../../general/display/charts/bars/color_bar.h"
|
||||
#include "../../../general/display/charts/pies/color_pie.h"
|
||||
|
|
@ -30,7 +31,7 @@ namespace
|
|||
{
|
||||
|
||||
AnalyticsPanelWidgetRegistrar registerManaBase{
|
||||
"manaBase", ManaBaseWidget::tr("Mana Base"),
|
||||
"manaBase", ManaBaseWidget::tr("Essence Production"),
|
||||
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
|
||||
|
||||
} // anonymous namespace
|
||||
|
|
@ -83,16 +84,12 @@ void ManaBaseWidget::updateDisplay()
|
|||
|
||||
// Choose display mode
|
||||
if (config.displayType == "bar") {
|
||||
const QList<QPair<QString, int>> sortedColors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(mapSorted);
|
||||
static const QHash<QString, QColor> colorMap = {
|
||||
{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)}, {"B", QColor(21, 11, 0)},
|
||||
{"R", QColor(211, 32, 42)}, {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)},
|
||||
};
|
||||
const QList<QPair<QString, int>> sortedColors = EssenceCharts::ordered(mapSorted);
|
||||
|
||||
for (const auto &[color, count] : sortedColors) {
|
||||
QString label = QString("%1 %2 (%3)").arg(color).arg(count).arg(cardCount.value(color));
|
||||
|
||||
BarWidget *bar = new BarWidget(label, count, highest, colorMap.value(color, Qt::gray), this);
|
||||
BarWidget *bar = new BarWidget(label, count, highest, EssenceCharts::colorFor(color), this);
|
||||
|
||||
barLayout->addWidget(bar);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#include "mana_curve_category_widget.h"
|
||||
|
||||
#include "../../../general/display/charts/bars/bar_chart_widget.h"
|
||||
#include "libcockatrice/utility/color.h"
|
||||
#include "../../../../essence_chart_colors.h"
|
||||
#include "libcockatrice/utility/qt_utils.h"
|
||||
#include "mana_curve_config.h"
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ void ManaCurveCategoryWidget::updateDisplay(int minCmc,
|
|||
|
||||
QVector<BarSegment> segments;
|
||||
if (val > 0) {
|
||||
segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)});
|
||||
segments.push_back({cat, val, cards, EssenceCharts::colorFor(cat)});
|
||||
}
|
||||
|
||||
catBars.push_back({QString::number(cmc), segments});
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ void ManaCurveConfigDialog::retranslateUi()
|
|||
{
|
||||
labelGroupBy->setText(tr("Group By:"));
|
||||
groupBy->setItemText(0, tr("type"));
|
||||
groupBy->setItemText(1, tr("color"));
|
||||
groupBy->setItemText(1, tr("Essence"));
|
||||
groupBy->setItemText(2, tr("subtype"));
|
||||
groupBy->setItemText(3, tr("power"));
|
||||
groupBy->setItemText(4, tr("toughness"));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#include "mana_curve_total_widget.h"
|
||||
|
||||
#include "../../../general/display/charts/bars/bar_chart_widget.h"
|
||||
#include "libcockatrice/utility/color.h"
|
||||
#include "../../../../essence_chart_colors.h"
|
||||
#include "mana_curve_config.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -71,7 +71,7 @@ void ManaCurveTotalWidget::updateDisplay(const QString &categoryName,
|
|||
cards = catIt->value(cmc);
|
||||
}
|
||||
|
||||
segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)});
|
||||
segments.push_back({category, value, cards, EssenceCharts::colorFor(category)});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace
|
|||
{
|
||||
|
||||
AnalyticsPanelWidgetRegistrar registerManaCurve{
|
||||
"manaCurve", ManaCurveWidget::tr("Mana Curve"),
|
||||
"manaCurve", ManaCurveWidget::tr("Essence Curve"),
|
||||
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }};
|
||||
|
||||
} // anonymous namespace
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ void ManaDevotionConfigDialog::retranslateUi()
|
|||
displayType->setItemText(1, tr("bar"));
|
||||
displayType->setItemText(2, tr("combinedBar"));
|
||||
|
||||
labelFilters->setText(tr("Filter Colors (optional):"));
|
||||
labelFilters->setText(tr("Filter Essence types (optional):"));
|
||||
}
|
||||
|
||||
void ManaDevotionConfigDialog::accept()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "mana_devotion_widget.h"
|
||||
|
||||
#include "../../../../essence_chart_colors.h"
|
||||
#include "../../../general/display/charts/bars/bar_widget.h"
|
||||
#include "../../../general/display/charts/bars/color_bar.h"
|
||||
#include "../../../general/display/charts/pies/color_pie.h"
|
||||
|
|
@ -29,7 +30,7 @@ namespace
|
|||
{
|
||||
|
||||
AnalyticsPanelWidgetRegistrar registerManaDevotion{
|
||||
"manaDevotion", ManaDevotionWidget::tr("Mana Devotion"),
|
||||
"manaDevotion", ManaDevotionWidget::tr("Essence Cost Symbols"),
|
||||
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }};
|
||||
|
||||
} // anonymous namespace
|
||||
|
|
@ -82,18 +83,13 @@ void ManaDevotionWidget::updateDisplay()
|
|||
mapSorted.insert(QString(it.key()), it.value());
|
||||
}
|
||||
|
||||
// Color map
|
||||
QHash<QChar, QColor> colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
|
||||
{'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
|
||||
{'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
|
||||
|
||||
// Choose display mode
|
||||
if (config.displayType == "bar") {
|
||||
// One BarWidget per devotion color
|
||||
for (auto c : devoMap.keys()) {
|
||||
QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c)));
|
||||
|
||||
BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this);
|
||||
BarWidget *bar = new BarWidget(label, devoMap[c], highest, EssenceCharts::colorFor(QString(c)), this);
|
||||
|
||||
barLayout->addWidget(bar);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,18 @@
|
|||
#include <QListWidgetItem>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class QWidget;
|
||||
|
||||
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
|
||||
static const QStringList kEssences = [] {
|
||||
QStringList codes;
|
||||
for (const QChar symbol : Essence::symbols()) {
|
||||
codes.append(QString(symbol));
|
||||
}
|
||||
return codes;
|
||||
}();
|
||||
|
||||
ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
|
||||
: QDialog(parent), analyzer(analyzer)
|
||||
|
|
@ -33,7 +40,7 @@ ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAna
|
|||
|
||||
filterList = new QListWidget(this);
|
||||
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
|
||||
filterList->addItems(kColors); // dynamic/fixed, no translation needed
|
||||
filterList->addItems(kEssences); // dynamic/fixed, no translation needed
|
||||
lay->addWidget(filterList);
|
||||
|
||||
showColorRows = new QCheckBox(this);
|
||||
|
|
@ -56,9 +63,9 @@ void ManaDistributionConfigDialog::retranslateUi()
|
|||
displayType->setItemText(0, tr("pie"));
|
||||
displayType->setItemText(1, tr("bar"));
|
||||
|
||||
labelFilters->setText(tr("Colors:"));
|
||||
labelFilters->setText(tr("Essence types:"));
|
||||
|
||||
showColorRows->setText(tr("Show per-color rows"));
|
||||
showColorRows->setText(tr("Show per-Essence rows"));
|
||||
|
||||
// QDialogButtonBox buttons are automatically translated
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,5 +48,5 @@ void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int p
|
|||
void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent)
|
||||
{
|
||||
productionBar->setValue(percent);
|
||||
productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards));
|
||||
productionLabel->setText(QString(tr("%1 Essence (%2 cards)")).arg(pips).arg(cards));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,18 +14,25 @@
|
|||
#include <QList>
|
||||
#include <QSharedPointer>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
namespace
|
||||
{
|
||||
AnalyticsPanelWidgetRegistrar registerManaDistribution{
|
||||
"manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"),
|
||||
"manaProdDevotion", ManaDistributionWidget::tr("Essence Production + Costs"),
|
||||
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
|
||||
static const QStringList kEssences = [] {
|
||||
QStringList codes;
|
||||
for (const QChar symbol : Essence::symbols()) {
|
||||
codes.append(QString(symbol));
|
||||
}
|
||||
return codes;
|
||||
}();
|
||||
|
||||
ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
|
||||
: AbstractAnalyticsPanelWidget(parent, analyzer)
|
||||
|
|
@ -49,7 +56,7 @@ ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisti
|
|||
row = new QHBoxLayout();
|
||||
containerLayout->addLayout(row);
|
||||
|
||||
for (const QString &c : kColors) {
|
||||
for (const QString &c : kEssences) {
|
||||
auto *w = new ManaDistributionSingleDisplayWidget(c, this);
|
||||
row->addWidget(w);
|
||||
rows[c] = w;
|
||||
|
|
@ -65,7 +72,7 @@ void ManaDistributionWidget::updateDisplay()
|
|||
const auto &prodPips = analyzer->getProductionPipCount();
|
||||
const auto &prodCards = analyzer->getProductionCardCount();
|
||||
|
||||
QStringList filtered = config.filters.isEmpty() ? kColors : config.filters;
|
||||
QStringList filtered = config.filters.isEmpty() ? kEssences : config.filters;
|
||||
|
||||
QMap<QString, int> devMap, prodMap;
|
||||
for (const QString &c : filtered) {
|
||||
|
|
@ -89,7 +96,7 @@ void ManaDistributionWidget::updateDisplay()
|
|||
productionBarTop->setColors(prodMap);
|
||||
}
|
||||
|
||||
for (const QString &c : kColors) {
|
||||
for (const QString &c : kEssences) {
|
||||
auto *w = rows.value(c);
|
||||
|
||||
if (!w) {
|
||||
|
|
@ -126,7 +133,7 @@ void ManaDistributionWidget::updateDisplay()
|
|||
QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent)
|
||||
{
|
||||
auto *dlg = new ManaDistributionConfigDialog(analyzer, parent);
|
||||
dlg->setWindowTitle(tr("Mana Distribution Settings"));
|
||||
dlg->setWindowTitle(tr("Essence Distribution Settings"));
|
||||
dlg->setFromConfig(config);
|
||||
|
||||
connect(dlg, &QDialog::accepted, [this, dlg]() {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "deck_list_model.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/deck_list/tree/deck_list_card_node.h"
|
||||
#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
|
||||
|
|
@ -127,68 +128,39 @@ void DeckListStatisticsAnalyzer::analyze()
|
|||
|
||||
QHash<QString, int> DeckListStatisticsAnalyzer::determineManaProduction(const QString &rulesText)
|
||||
{
|
||||
QHash<QString, int> manaCounts = {{"W", 0}, {"U", 0}, {"B", 0}, {"R", 0}, {"G", 0}, {"C", 0}};
|
||||
|
||||
QString text = rulesText.toLower(); // Normalize case for matching
|
||||
|
||||
// Quick keyword-based checks for any color and colorless mana
|
||||
if (text.contains("{t}: add one mana of any color") || text.contains("add one mana of any color")) {
|
||||
for (const auto &color : {QStringLiteral("W"), QStringLiteral("U"), QStringLiteral("B"), QStringLiteral("R"),
|
||||
QStringLiteral("G")}) {
|
||||
manaCounts[color]++;
|
||||
}
|
||||
}
|
||||
if (text.contains("{t}: add {c}") || text.contains("add one colorless mana")) {
|
||||
manaCounts["C"]++;
|
||||
QHash<QString, int> result;
|
||||
for (const QChar code : Essence::symbols()) {
|
||||
result.insert(QString(code), 0);
|
||||
}
|
||||
|
||||
// Optimized regex for specific mana symbols
|
||||
static const QRegularExpression specificColorRegex(R"(\{T\}:\s*Add\s*\{([WUBRG])\})");
|
||||
QRegularExpressionMatch match = specificColorRegex.match(rulesText);
|
||||
if (match.hasMatch()) {
|
||||
manaCounts[match.captured(1)]++;
|
||||
}
|
||||
// Explicit Essence grants only. Inferring additional resources from MTG
|
||||
// tap/mana rules would invent Continuum Universes gameplay semantics.
|
||||
// E.g. "Add {M}" / "add {N}" both work.
|
||||
static const QRegularExpression grantedEssence(
|
||||
QStringLiteral(R"(\badd\s*\{([MSOGEAQCJBRLVTPN])\})"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
return manaCounts;
|
||||
auto matches = grantedEssence.globalMatch(rulesText);
|
||||
while (matches.hasNext()) {
|
||||
const QString code = matches.next().captured(1).toUpper();
|
||||
result[code]++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<char, int> DeckListStatisticsAnalyzer::countManaSymbols(const QString &manaString)
|
||||
std::unordered_map<char, int> DeckListStatisticsAnalyzer::countManaSymbols(const QString &essenceCost)
|
||||
{
|
||||
std::unordered_map<char, int> manaCounts = {{'W', 0}, {'U', 0}, {'B', 0}, {'R', 0}, {'G', 0}};
|
||||
|
||||
int len = manaString.length();
|
||||
for (int i = 0; i < len; ++i) {
|
||||
if (manaString[i] == '{') {
|
||||
++i; // Move past '{'
|
||||
if (i < len && manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) {
|
||||
char mana1 = manaString[i].toLatin1();
|
||||
++i; // Move to next character
|
||||
if (i < len && manaString[i] == '/') {
|
||||
++i; // Move past '/'
|
||||
if (i < len && manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) {
|
||||
char mana2 = manaString[i].toLatin1();
|
||||
manaCounts[mana1]++;
|
||||
manaCounts[mana2]++;
|
||||
} else {
|
||||
// Handle cases like "{W/}" where second part is invalid
|
||||
manaCounts[mana1]++;
|
||||
}
|
||||
} else {
|
||||
manaCounts[mana1]++;
|
||||
}
|
||||
}
|
||||
// Ensure we always skip to the closing '}'
|
||||
while (i < len && manaString[i] != '}') {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// Check if the character is a standalone mana symbol (not inside {})
|
||||
else if (manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) {
|
||||
manaCounts[manaString[i].toLatin1()]++;
|
||||
}
|
||||
std::unordered_map<char, int> result;
|
||||
for (const QChar code : Essence::symbols()) {
|
||||
result.emplace(code.toLatin1(), 0);
|
||||
}
|
||||
|
||||
return manaCounts;
|
||||
for (const QString &token : Essence::parseCost(essenceCost)) {
|
||||
if (token.size() == 1 && Essence::isSymbol(token.at(0))) {
|
||||
result[token.at(0).toLatin1()]++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Hypergeometric probability: P(X=k)
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ private:
|
|||
std::unordered_map<int, int> manaCurveMap;
|
||||
std::unordered_map<char, int> manaDevotionMap;
|
||||
|
||||
QHash<QString, int> devotionPipCount; // W/U/B/R/G total symbols
|
||||
QHash<QString, int> devotionPipCount; // Counts for the fifteen Continuum Universes Essence codes
|
||||
QHash<QString, int> devotionCardCount; // how many cards provide devotion
|
||||
|
||||
QHash<QString, int> productionPipCount; // mana produced by cards
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "color_bar.h"
|
||||
|
||||
#include "libcockatrice/utility/color.h"
|
||||
#include "../../../../../essence_chart_colors.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QLinearGradient>
|
||||
|
|
@ -19,14 +19,14 @@
|
|||
#include <utility>
|
||||
|
||||
ColorBar::ColorBar(const QMap<QString, int> &_colors, QWidget *parent)
|
||||
: QWidget(parent), colors(GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors))
|
||||
: QWidget(parent), colors(EssenceCharts::ordered(_colors))
|
||||
{
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void ColorBar::setColors(const QMap<QString, int> &_colors)
|
||||
{
|
||||
colors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors);
|
||||
colors = EssenceCharts::ordered(_colors);
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -150,19 +150,5 @@ QString ColorBar::tooltipForPosition(int x) const
|
|||
|
||||
QColor ColorBar::colorFromName(const QString &name) const
|
||||
{
|
||||
static QMap<QString, QColor> map = {
|
||||
{"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)},
|
||||
{"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)},
|
||||
};
|
||||
|
||||
if (map.contains(name)) {
|
||||
return map[name];
|
||||
}
|
||||
|
||||
QColor c(name);
|
||||
if (!c.isValid()) {
|
||||
c = Qt::gray;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
return EssenceCharts::colorFor(name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "color_pie.h"
|
||||
|
||||
#include "libcockatrice/utility/color.h"
|
||||
#include "../../../../../essence_chart_colors.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QFontMetrics>
|
||||
|
|
@ -20,14 +20,14 @@
|
|||
#include <utility>
|
||||
|
||||
ColorPie::ColorPie(const QMap<QString, int> &_colors, QWidget *parent)
|
||||
: QWidget(parent), colors(GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors))
|
||||
: QWidget(parent), colors(EssenceCharts::ordered(_colors))
|
||||
{
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void ColorPie::setColors(const QMap<QString, int> &_colors)
|
||||
{
|
||||
colors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors);
|
||||
colors = EssenceCharts::ordered(_colors);
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -184,19 +184,5 @@ QString ColorPie::tooltipForPoint(const QPoint &pt) const
|
|||
|
||||
QColor ColorPie::colorFromName(const QString &name) const
|
||||
{
|
||||
static QMap<QString, QColor> map = {
|
||||
{"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)},
|
||||
{"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)},
|
||||
};
|
||||
|
||||
if (map.contains(name)) {
|
||||
return map[name];
|
||||
}
|
||||
|
||||
QColor c(name);
|
||||
if (!c.isValid()) {
|
||||
c = Qt::gray;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
return EssenceCharts::colorFor(name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,44 @@
|
|||
#include "visual_database_display_color_filter_widget.h"
|
||||
|
||||
#include "../cards/additional_info/mana_symbol_widget.h"
|
||||
#include "../cards/additional_info/essence_filter_picker.h"
|
||||
#include "filter_tree_model.h"
|
||||
#include "libcockatrice/filters/filter_card.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QComboBox>
|
||||
#include <QDebug>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QSharedPointer>
|
||||
#include <QSizePolicy>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include <libcockatrice/filters/filter_tree.h>
|
||||
|
||||
/**
|
||||
* This widget provides a graphical control element for the CardFilter::Attr::AttrColor filters applied to the filter
|
||||
* model.
|
||||
* @param parent The Qt Widget that this widget will parent to
|
||||
* @param _filterModel The filter model that this widget will manipulate
|
||||
*/
|
||||
VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(QWidget *parent,
|
||||
FilterTreeModel *_filterModel)
|
||||
: QWidget(parent), filterModel(_filterModel), layout(new QHBoxLayout(this))
|
||||
{
|
||||
setLayout(layout);
|
||||
layout->setSpacing(5);
|
||||
layout->setSpacing(3);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QString fullColorIdentity = "WUBRG";
|
||||
for (const QChar &color : fullColorIdentity) {
|
||||
auto *manaSymbol = new ManaSymbolWidget(this, color, false, true);
|
||||
manaSymbol->setFixedWidth(25);
|
||||
// Share the same compact Essence selection UI as Visual Deck Storage.
|
||||
// A 15-icon horizontal row cannot fit the database filter toolbar.
|
||||
essencePicker = new EssenceFilterPicker(this);
|
||||
layout->addWidget(essencePicker);
|
||||
// FlowWidget measures THIS parent widget, not its nested button. Give it
|
||||
// an explicit size hint that cannot clip the picker.
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
||||
setMinimumWidth(essencePicker->width());
|
||||
|
||||
layout->addWidget(manaSymbol);
|
||||
connect(essencePicker, &EssenceFilterPicker::essenceToggled, this,
|
||||
&VisualDatabaseDisplayColorFilterWidget::handleColorToggled);
|
||||
|
||||
// Connect the color toggled signal
|
||||
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this,
|
||||
&VisualDatabaseDisplayColorFilterWidget::handleColorToggled);
|
||||
}
|
||||
connect(essencePicker, &EssenceFilterPicker::selectionCleared, this, [this]() {
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
});
|
||||
|
||||
modeComboBox = new QComboBox(this);
|
||||
layout->addWidget(modeComboBox);
|
||||
essencePicker->addPopupControl(modeComboBox);
|
||||
|
||||
connect(modeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&VisualDatabaseDisplayColorFilterWidget::updateFilterMode);
|
||||
|
|
@ -50,108 +46,83 @@ VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(Q
|
|||
connect(filterModel, &FilterTreeModel::layoutChanged, this,
|
||||
[this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); });
|
||||
|
||||
// Call retranslateUi to set the initial text
|
||||
retranslateUi();
|
||||
syncWithFilterModel();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayColorFilterWidget::retranslateUi()
|
||||
{
|
||||
essencePicker->retranslateUi();
|
||||
|
||||
modeComboBox->blockSignals(true);
|
||||
modeComboBox->clear();
|
||||
|
||||
modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch));
|
||||
modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes));
|
||||
modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude));
|
||||
modeComboBox->setToolTip(tr("How selected and unselected Essence types are combined in the filter"));
|
||||
|
||||
modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter"));
|
||||
|
||||
// Restore current mode
|
||||
const int index = modeComboBox->findData(QVariant::fromValue(currentMode));
|
||||
if (index >= 0) {
|
||||
modeComboBox->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
modeComboBox->blockSignals(false);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active)
|
||||
void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar essence, bool active)
|
||||
{
|
||||
if (active) {
|
||||
addFilter(color);
|
||||
addFilter(essence);
|
||||
} else {
|
||||
removeFilter(color);
|
||||
removeFilter(essence);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color)
|
||||
void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar essence)
|
||||
{
|
||||
QString colorString = color;
|
||||
QString typeStr;
|
||||
|
||||
// Remove previous filters
|
||||
|
||||
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
QList<const CardFilter *> matchingFilters;
|
||||
|
||||
for (const CardFilter *filter : allColorFilters) {
|
||||
if (filter->term() == color) {
|
||||
matchingFilters.append(filter);
|
||||
QString essenceString(essence);
|
||||
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
for (const CardFilter *filter : filters) {
|
||||
if (filter->term() == essenceString) {
|
||||
filterModel->removeFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
for (const CardFilter *filter : matchingFilters) {
|
||||
filterModel->removeFilter(filter);
|
||||
}
|
||||
|
||||
// Add actual filter
|
||||
|
||||
switch (currentMode) {
|
||||
case FilterMode::ExactMatch:
|
||||
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor));
|
||||
filterModel->addFilter(
|
||||
new CardFilter(essenceString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor));
|
||||
break;
|
||||
|
||||
case FilterMode::Includes:
|
||||
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
|
||||
break;
|
||||
|
||||
case FilterMode::IncludeExclude:
|
||||
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
|
||||
filterModel->addFilter(
|
||||
new CardFilter(essenceString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color)
|
||||
void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar essence)
|
||||
{
|
||||
QString colorString = color;
|
||||
|
||||
// Remove inclusion filters
|
||||
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
QList<const CardFilter *> matchingFilters;
|
||||
|
||||
for (const CardFilter *filter : allColorFilters) {
|
||||
if (filter->term() == color) {
|
||||
matchingFilters.append(filter);
|
||||
QString essenceString(essence);
|
||||
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
for (const CardFilter *filter : filters) {
|
||||
if (filter->term() == essenceString) {
|
||||
filterModel->removeFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
for (const CardFilter *filter : matchingFilters) {
|
||||
filterModel->removeFilter(filter);
|
||||
// The empty selection means "Any Essence", not "exclude everything".
|
||||
if (essencePicker->activeEssences().isEmpty()) {
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add exclusion filters if the mode demands it
|
||||
switch (currentMode) {
|
||||
case FilterMode::ExactMatch:
|
||||
filterModel->addFilter(
|
||||
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
|
||||
break;
|
||||
|
||||
case FilterMode::IncludeExclude:
|
||||
filterModel->addFilter(
|
||||
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
|
||||
new CardFilter(essenceString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
|
||||
break;
|
||||
|
||||
case FilterMode::Includes:
|
||||
// No exclusion in Includes mode
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -170,10 +141,12 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
|
|||
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
|
||||
const QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
|
||||
|
||||
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
|
||||
handleColorToggled(manaSymbolWidget->getSymbolChar(), manaSymbolWidget->isColorActive());
|
||||
const QSet<QChar> selected = essencePicker->activeEssences();
|
||||
// No selections should never generate 15 mutually excluding filters.
|
||||
if (!selected.isEmpty()) {
|
||||
for (const QChar essence : Essence::symbols()) {
|
||||
handleColorToggled(essence, selected.contains(essence));
|
||||
}
|
||||
}
|
||||
|
||||
filterModel->blockSignals(false);
|
||||
|
|
@ -184,79 +157,44 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
|
|||
emit filterModeChanged(currentMode);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayColorFilterWidget::setManaSymbolActive(QChar color, bool active)
|
||||
void VisualDatabaseDisplayColorFilterWidget::setEssenceActive(QChar essence, bool active)
|
||||
{
|
||||
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
|
||||
|
||||
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
|
||||
if (manaSymbolWidget->getSymbolChar() == color) {
|
||||
manaSymbolWidget->setColorActive(active);
|
||||
}
|
||||
}
|
||||
essencePicker->setEssenceActive(essence, active);
|
||||
}
|
||||
|
||||
QList<QChar> VisualDatabaseDisplayColorFilterWidget::getActiveColors()
|
||||
{
|
||||
QList<QChar> activeColors;
|
||||
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
|
||||
|
||||
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
|
||||
if (manaSymbolWidget->isColorActive()) {
|
||||
activeColors.append(manaSymbolWidget->getSymbolChar());
|
||||
QList<QChar> result;
|
||||
const QSet<QChar> selected = essencePicker->activeEssences();
|
||||
for (const QChar essence : Essence::symbols()) {
|
||||
if (selected.contains(essence)) {
|
||||
result.append(essence);
|
||||
}
|
||||
}
|
||||
|
||||
return activeColors;
|
||||
return result;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
|
||||
|
||||
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
|
||||
|
||||
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
|
||||
bool found = false;
|
||||
for (const CardFilter *filter : allColorFilters) {
|
||||
if (manaSymbolWidget->getSymbolChar() == filter->term()) {
|
||||
switch (currentMode) {
|
||||
case FilterMode::ExactMatch:
|
||||
switch (filter->type()) {
|
||||
case CardFilter::Type::TypeAnd:
|
||||
setManaSymbolActive(filter->term().at(0), true);
|
||||
break;
|
||||
default:
|
||||
setManaSymbolActive(filter->term().at(0), false);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case FilterMode::Includes:
|
||||
switch (filter->type()) {
|
||||
case CardFilter::Type::TypeOr:
|
||||
setManaSymbolActive(filter->term().at(0), true);
|
||||
break;
|
||||
default:
|
||||
setManaSymbolActive(filter->term().at(0), false);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case FilterMode::IncludeExclude:
|
||||
switch (filter->type()) {
|
||||
case CardFilter::Type::TypeOr:
|
||||
setManaSymbolActive(filter->term().at(0), true);
|
||||
break;
|
||||
default:
|
||||
setManaSymbolActive(filter->term().at(0), false);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
found = true;
|
||||
for (const QChar essence : Essence::symbols()) {
|
||||
bool selected = false;
|
||||
for (const CardFilter *filter : filters) {
|
||||
if (filter->term() != QString(essence)) {
|
||||
continue;
|
||||
}
|
||||
if (currentMode == FilterMode::ExactMatch) {
|
||||
selected = filter->type() == CardFilter::Type::TypeAnd;
|
||||
} else {
|
||||
selected = filter->type() == CardFilter::Type::TypeOr;
|
||||
}
|
||||
if (selected) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
setManaSymbolActive(manaSymbolWidget->getSymbolChar(), false);
|
||||
}
|
||||
// Signals are blocked inside the picker; loading saved filters must
|
||||
// not mutate the FilterTreeModel again.
|
||||
setEssenceActive(essence, selected);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
class FilterTreeModel;
|
||||
class QComboBox;
|
||||
class QHBoxLayout;
|
||||
class EssenceFilterPicker;
|
||||
|
||||
class VisualDatabaseDisplayColorFilterCircleWidget : public QWidget
|
||||
{
|
||||
|
|
@ -63,7 +64,7 @@ private slots:
|
|||
void addFilter(QChar color);
|
||||
void removeFilter(QChar color);
|
||||
void updateFilterMode();
|
||||
void setManaSymbolActive(QChar color, bool active);
|
||||
void setEssenceActive(QChar essence, bool active);
|
||||
QList<QChar> getActiveColors();
|
||||
void syncWithFilterModel();
|
||||
|
||||
|
|
@ -71,6 +72,7 @@ private:
|
|||
FilterTreeModel *filterModel;
|
||||
QHBoxLayout *layout;
|
||||
QComboBox *modeComboBox;
|
||||
EssenceFilterPicker *essencePicker = nullptr;
|
||||
FilterMode currentMode = FilterMode::Includes; // Default mode
|
||||
QtUtils::Debouncer syncDebouncer{100};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
#include "deck_preview_color_identity_filter_widget.h"
|
||||
|
||||
#include "../../cards/additional_info/mana_symbol_widget.h"
|
||||
#include "../../cards/additional_info/essence_filter_picker.h"
|
||||
#include "../visual_deck_storage_sort_filter_proxy_model.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QSizePolicy>
|
||||
#include <QSet>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
|
||||
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent)
|
||||
|
|
@ -16,34 +16,30 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidg
|
|||
layout->setSpacing(5);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QString fullColorIdentity = "WUBRG";
|
||||
for (const QChar &color : fullColorIdentity) {
|
||||
auto *manaSymbol = new ManaSymbolWidget(this, color, false, true);
|
||||
manaSymbol->setFixedWidth(25);
|
||||
// Fifteen Essence codes don't fit as inline buttons beside sorting and
|
||||
// search. The popup keeps all codes selectable without expanding the row.
|
||||
essencePicker = new EssenceFilterPicker(this);
|
||||
layout->addWidget(essencePicker);
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
||||
setMinimumWidth(essencePicker->width());
|
||||
|
||||
layout->addWidget(manaSymbol);
|
||||
|
||||
// Initialize the activeColors map
|
||||
activeColors[color] = false;
|
||||
|
||||
// Connect the color toggled signal
|
||||
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this,
|
||||
&DeckPreviewColorIdentityFilterWidget::handleColorToggled);
|
||||
}
|
||||
connect(essencePicker, &EssenceFilterPicker::essenceToggled, this,
|
||||
[this](QChar /*essence*/, bool /*active*/) { emit activeColorsChanged(); });
|
||||
connect(essencePicker, &EssenceFilterPicker::selectionCleared, this,
|
||||
[this]() { emit activeColorsChanged(); });
|
||||
|
||||
toggleButton = new QPushButton(this);
|
||||
layout->addWidget(toggleButton);
|
||||
|
||||
// Connect the button's clicked signal
|
||||
essencePicker->addPopupControl(toggleButton);
|
||||
connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode);
|
||||
|
||||
// Call retranslateUi to set the initial text
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::retranslateUi()
|
||||
{
|
||||
// Set the toggle button text based on the current mode
|
||||
essencePicker->retranslateUi();
|
||||
|
||||
// Preserve existing Exact / Includes / Excludes filter semantics.
|
||||
switch (filterMode) {
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
|
||||
toggleButton->setText(tr("Mode: Exact Match"));
|
||||
|
|
@ -55,32 +51,21 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi()
|
|||
toggleButton->setText(tr("Mode: Excludes"));
|
||||
break;
|
||||
}
|
||||
toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)"));
|
||||
toggleButton->setToolTip(tr("Essence filter mode (exact match, includes, or excludes)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The colors that are currently toggled on.
|
||||
*/
|
||||
QSet<QChar> DeckPreviewColorIdentityFilterWidget::getActiveColors() const
|
||||
{
|
||||
QSet<QChar> activeColorSet;
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
if (it.value()) {
|
||||
activeColorSet.insert(it.key());
|
||||
}
|
||||
}
|
||||
return activeColorSet;
|
||||
return essencePicker->activeEssences();
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active)
|
||||
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar /*essence*/, bool /*active*/)
|
||||
{
|
||||
activeColors[color] = active;
|
||||
emit activeColorsChanged();
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
|
||||
{
|
||||
// Cycle through the modes
|
||||
switch (filterMode) {
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
|
||||
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes;
|
||||
|
|
@ -93,6 +78,6 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
|
|||
break;
|
||||
}
|
||||
|
||||
retranslateUi(); // Update the button text
|
||||
retranslateUi();
|
||||
emit filterModeChanged(filterMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@
|
|||
#include "../visual_deck_storage_sort_filter_proxy_model.h"
|
||||
|
||||
#include <QChar>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QWidget>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QHBoxLayout;
|
||||
class QPushButton;
|
||||
class EssenceFilterPicker;
|
||||
|
||||
class DeckPreviewColorIdentityFilterWidget : public QWidget
|
||||
{
|
||||
|
|
@ -57,7 +57,7 @@ private slots:
|
|||
private:
|
||||
QHBoxLayout *layout;
|
||||
QPushButton *toggleButton;
|
||||
QMap<QChar, bool> activeColors;
|
||||
EssenceFilterPicker *essencePicker = nullptr;
|
||||
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,16 +10,18 @@ Cockatrice's three visual table rows are reused as **Front**, **Support**, and *
|
|||
|
||||
## Optional color customization
|
||||
|
||||
Create `battlefield.ini` in the active Cockatrice theme directory:
|
||||
The default zone colors are **Front `#dc143c` (Crimson)**, **Support `#c3b091`**, and **Command `#0a0e3f`**.
|
||||
|
||||
To override the defaults, create `battlefield.ini` in your active **Continuumatrice** theme directory:
|
||||
|
||||
```ini
|
||||
[Zones]
|
||||
Front = #A08E343D
|
||||
Support = #A0286981
|
||||
Command = #A05E428C
|
||||
Front = #dc143c
|
||||
Support = #c3b091
|
||||
Command = #0a0e3f
|
||||
```
|
||||
|
||||
The Qt color notation is `#AARRGGBB`, including transparency. Without this file, built-in red, teal, and purple shades are used. Reload the theme to apply changes. The default layout places Front closest to the opposing player's battlefield and Command closest to each player's hand. As in Cockatrice, the **Invert vertical coordinate** setting reverses the row placement (and its labels) when enabled.
|
||||
Qt accepts `#RRGGBB` for opaque colors and `#AARRGGBB` to specify transparency. The built-in defaults above are opaque; a theme can set translucent overrides explicitly. An existing `battlefield.ini` still takes precedence over these defaults. Reload the theme to apply changes. The default layout places Front closest to the opposing player's battlefield and Command closest to each player's hand. As in Cockatrice, the **Invert vertical coordinate** setting reverses the row placement (and its labels) when enabled.
|
||||
|
||||
## Font
|
||||
|
||||
|
|
|
|||
69
doc/continuumatrice-card-fields.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Continuumatrice CCG card fields and numeric sorting
|
||||
|
||||
This document covers **static card-definition metadata** loaded from Continuumatrice XML v4. It does not change mutable game resources, damage, armour degradation, campaign state or server match mechanics.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Continuumatrice field | Native XML v4 property | Field in existing Continuum MSE game | Legacy import fallback | Sorting |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Identity | `identity` | `alias` | `alias` | Locale-aware text |
|
||||
| Attack (ATK) | `attack` | `strength` | `strength`, or Attack side of `pt` | Numeric |
|
||||
| Armour | `armour` | `armor` | `armor` | Numeric |
|
||||
| Health (HP) | `health` | `health` | Health side of `pt` | Numeric |
|
||||
| ATK/HP | Derived from Attack and Health | MSE `strength` + `health` | `pt` via the individual stat fallbacks | Attack numeric first, then Health numeric |
|
||||
|
||||
**Identity** is the CCG's **card alias**. It must not be confused with `essence` (which is the distinct Essence resource/type identity). The `Name` column still contains the actual card name.
|
||||
|
||||
The XML importer already supports arbitrary text properties under `<prop>`, so adding these properties does not require a schema version change.
|
||||
|
||||
## Example
|
||||
|
||||
```xml
|
||||
<card>
|
||||
<name>Example Character</name>
|
||||
<text>Static card data demonstration only.</text>
|
||||
<prop>
|
||||
<type>Character</type>
|
||||
<identity>The Guardian</identity>
|
||||
<attack>10</attack>
|
||||
<armour>3</armour>
|
||||
<health>12</health>
|
||||
<essence>M</essence>
|
||||
</prop>
|
||||
<set>EXAMPLE</set>
|
||||
<tablerow>0</tablerow>
|
||||
</card>
|
||||
```
|
||||
|
||||
The table view displays ATK/HP as `10/12`, with separate **Attack**, **Armour**, and **Health** columns. If a card has no authored stats, the combined display remains blank. When one stat is omitted, the missing side is shown as `?`.
|
||||
|
||||
The database's sort dropdown discovers model columns dynamically, so the new fields are available for ascending/descending sorting. Numeric fields use numeric comparisons; the combined ATK/HP column sorts by Attack first and Health second. The former `P/T` column position is retained internally for compatibility with existing saved table views.
|
||||
|
||||
## Exporter compatibility
|
||||
|
||||
The matching Magic Set Editor exporter change is in `Continuum-Universes-MSE-Templates`:
|
||||
|
||||
- `card.alias` → `<identity>`
|
||||
- `card.strength` → `<attack>`
|
||||
- `card.armor` → `<armour>`
|
||||
- `card.health` → `<health>`
|
||||
|
||||
The MSE internal field identifiers are **not renamed**: that would otherwise risk breaking existing `.mse-set` files, render scripts and card styles.
|
||||
|
||||
Older XML exports with `strength`, `armor`, `alias`, or `pt` remain readable. Native properties take precedence whenever present; no automatic rewriting of existing files occurs.
|
||||
|
||||
## Test checklist
|
||||
|
||||
```bash
|
||||
cmake --build build --target continuumatrice carddatabase_test model_contract_test --parallel 16
|
||||
ctest --test-dir build -R 'CardInfoTest.ContinuumIdentityAndStatsHaveNativeAndLegacyPropertyAccess|ModelContract.ContinuumCardStatsAreSortableAndLabeled' --output-on-failure
|
||||
```
|
||||
|
||||
- [ ] Import an MSE-exported set and verify Identity, Attack, Armour, Health and ATK/HP columns.
|
||||
- [ ] Check Attack `9` precedes `10` and Armour `2` precedes `10`.
|
||||
- [ ] Check ATK/HP sorts on Attack then Health.
|
||||
- [ ] Verify legacy fields are still readable and an empty field is not assumed to mean zero.
|
||||
- [ ] Verify the card details pane displays the new names, including **Armour**.
|
||||
- [ ] Verify the existing Essence filter still operates independently of the Alias/Identity property.
|
||||
|
||||
**Status:** Source changes committed; native compilation, tests and MSE GUI export still need local runtime verification.
|
||||
111
doc/continuumatrice-essence.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Continuumatrice Essence symbols (CCG card catalogue)
|
||||
|
||||
Continuumatrice uses **Essence** for card resources, not Magic: The Gathering mana. The canonical Essence symbol order is:
|
||||
|
||||
```text
|
||||
M S O G E A Q C J B R L V T P N
|
||||
```
|
||||
|
||||
These are **16 distinct symbols**. `N` is explicitly **colorless Essence**. The non-colorless codes use the existing MSE gemstone mappings (including **J = Jasper**), but the client does not assign their final artwork, gameplay rules or resource behavior. Do not infer anything from similarly named MTG mana letters. All Essence codes are uppercase.
|
||||
|
||||
## Native Continuumatrice XML v4
|
||||
|
||||
The parser supports arbitrary XML `<prop>` entries, so no change to the existing XML v4 schema format is needed. These three native property names are defined in `libcockatrice/card/game_specific_terms.h`:
|
||||
|
||||
| Property | Meaning |
|
||||
| --- | --- |
|
||||
| `essencecost` | Card's explicitly authored Essence cost, e.g. `{M}{S}{N}` |
|
||||
| `essence` | Card's explicitly authored Essence identity/types, e.g. `MSN` |
|
||||
| `essencevalue` | Optional numeric aggregate value for existing cost-curve UI; this is not calculated from cost |
|
||||
|
||||
**Test-only sample**, not a playable/canonical card:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<continuumatrice_carddatabase version="4">
|
||||
<sets>
|
||||
<set><name>CTEST</name><longname>Essence Import Test</longname></set>
|
||||
</sets>
|
||||
<cards>
|
||||
<card>
|
||||
<name>Essence Sample Card</name>
|
||||
<text>Test Essence symbol rendering only.</text>
|
||||
<prop>
|
||||
<type>Test Card</type>
|
||||
<essencecost>{M}{S}{N}</essencecost>
|
||||
<essence>MSN</essence>
|
||||
</prop>
|
||||
<set num="001">CTEST</set>
|
||||
<tablerow>1</tablerow>
|
||||
</card>
|
||||
</cards>
|
||||
</continuumatrice_carddatabase>
|
||||
```
|
||||
|
||||
Do not automatically derive `essence` or `essencevalue` from `essencecost` until gameplay rules define the correct relationship. The card database **stores metadata**, not mutable match resource balances.
|
||||
|
||||
Previously authored XML `<manacost>`, `<colors>`, and `<cmc>` properties remain readable by the inherited `CardInfo` API for compatibility, but new Continuumatrice cards should use the native properties. Format compatibility does **not** mean MTG `W` or `U` are accepted as Essence symbols.
|
||||
|
||||
## Icons and custom asset assignment
|
||||
|
||||
All 16 Essence letters have small bundled SVG **placeholder** icons under:
|
||||
|
||||
```text
|
||||
cockatrice/resources/icons/essence/
|
||||
M.svg S.svg O.svg G.svg E.svg A.svg Q.svg C.svg J.svg B.svg
|
||||
R.svg L.svg V.svg T.svg P.svg N.svg
|
||||
```
|
||||
|
||||
Each is rendered using an independent `icons/essence/` asset namespace. In the currently selected Continuumatrice theme you may override any SVG with an SVG, PNG, JPG or JPEG using the same stem:
|
||||
|
||||
```text
|
||||
~/.local/share/Continuumatrice/themes/<YourTheme>/icons/essence/
|
||||
M.svg
|
||||
S.png
|
||||
N.svg
|
||||
```
|
||||
|
||||
The current bundled placeholders show just the letter. **J = Jasper** (matching the existing Continuum MSE authoring templates). `N` is neutral gray; the others use a shared neutral treatment. They are **not** final Essence artwork or a commitment to elemental color assignments. The chart interface uses a separately generated *chart-only* palette so sixteen statistical series can be distinguished.
|
||||
|
||||
## Features updated
|
||||
|
||||
- Card database properties: native Essence cost, identity and optional value, with fallbacks for older XML.
|
||||
- Card information display and deck editor column headers: `Essence`, `Essence Cost`, `Essence Value`.
|
||||
- Visual Deck Storage and Visual Database filters share a compact `Essence: Any` dropdown with a 4×4 grid of all 16 icons. Select multiple icons without closing the popup, clear selections within the popup, and change filtering mode **inside that same popup**. Only one compact control occupies the toolbar. The dropdown summarizes selected Essence codes and supports N independently from C.
|
||||
- Cost parser: single Essence symbols in bare form (`MSN`) or braces (`{M}{S}{N}`); numeric tokens may be preserved as generic amounts.
|
||||
- Deck identity: unique, ordered Essence codes (including `N`).
|
||||
- Search filters: filter for specific Essence codes, or `colorless` meaning `N`. An empty identity is not automatically `N`.
|
||||
- Analytics: counts all 16 Essence cost symbols. Production counts only literal `Add {M}`-style grants to avoid guessing any resource rules.
|
||||
|
||||
Hybrid, phyrexian, and other MTG-specific mana-cost syntax are **not** introduced as Essence mechanics. Nor is automatic calculation of resource costs, card affordability, or game turn rules included.
|
||||
|
||||
## Compact Essence filter UI
|
||||
|
||||
The former five-button MTG `WUBRG` filters are replaced in both Visual Deck Storage and Visual Database. On either screen, click `Essence: Any` to open a **4-column × 4-row** grid of the complete Essence set. Each icon can be toggled independently and the menu stays open for multiple selections. Use **Clear Essence selection** to reset. The button summarizes your selection, and the filtering mode remains available **inside** the popup (Exact Match / Includes / Excludes in deck storage, or Include / Exclude in the database).
|
||||
|
||||
The picker is a single reusable Qt widget (`essence_filter_picker.h/.cpp`). This keeps both screens synchronized with `Essence::symbols()` without repeating a hand-maintained UI list. The UI uses SVG assets from `icons/essence/`, never old MTG `icons/mana/` assets.
|
||||
|
||||
|
||||
## Build and verification
|
||||
|
||||
From `feature/continuumatrice-essence`:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build --fresh -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DWITH_CLIENT=ON -DWITH_SERVER=ON -DWITH_ORACLE=OFF -DTEST=ON
|
||||
cmake --build build --target continuumatrice continuum-servatrice essence_types_test --parallel 16
|
||||
ctest --test-dir build -R EssenceTypesTest --output-on-failure
|
||||
./build/continuumatrice/continuumatrice
|
||||
```
|
||||
|
||||
Verify that all sixteen icons render, `N` loads and filters separately from `C`, the XML importer accepts `essencecost` and `essence`, and the analytics show no WUBRG assumptions.
|
||||
|
||||
**Status:** Source-level implementation committed. User-side Qt compilation and visual/runtime testing remain required.
|
||||
|
||||
## November UI stability fixes
|
||||
|
||||
The database toolbar now keeps its Essence filtering mode **inside** the picker popup, rather than forcing two separate controls into a wrapping flow layout. Both Visual Deck Storage and Visual Database use one compact toolbar button with a selection summary and tooltip. The popup grid is unchanged (16 icons, four columns × four rows).
|
||||
|
||||
When the final selected Essence is cleared, the filter becomes **Any**, including in Exact Match / Include-Exclude mode; it must not leave behind exclusions for all sixteen symbols.
|
||||
|
||||
A new Continuumatrice profile starts with a valid, deliberately empty XML card catalogue. The in-memory database now treats a successful empty snapshot as `LoadStatus::Ok`, not `NotLoaded`. This allows the card browser and toolbar to initialize even before the user's first MSE card import. `carddatabase_test` covers this distinction.
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
set(HEADERS
|
||||
libcockatrice/card/card_info.h
|
||||
libcockatrice/card/essence_types.h
|
||||
libcockatrice/card/card_info_comparator.h
|
||||
libcockatrice/card/card_localization.h
|
||||
libcockatrice/card/lazy_properties_hash.h
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_info.h"
|
||||
|
||||
#include "game_specific_terms.h"
|
||||
#include "essence_types.h"
|
||||
#include "libcockatrice/card/lazy_properties_hash.h"
|
||||
#include "printing/printing_info.h"
|
||||
#include "relation/card_relation.h"
|
||||
|
|
@ -304,17 +305,34 @@ void CardInfo::setCardType(const QString &value)
|
|||
{
|
||||
setProperty(Mtg::CardType, value);
|
||||
}
|
||||
QString CardInfo::getEssenceValue() const
|
||||
{
|
||||
return hasProperty(EssenceProperties::Value) ? getProperty(EssenceProperties::Value)
|
||||
: getProperty(Mtg::ConvertedManaCost);
|
||||
}
|
||||
QString CardInfo::getEssenceTypes() const
|
||||
{
|
||||
const QString value = hasProperty(EssenceProperties::Types)
|
||||
? getProperty(EssenceProperties::Types)
|
||||
: getProperty(Mtg::Colors);
|
||||
return Essence::normalizeIdentity(value);
|
||||
}
|
||||
QString CardInfo::getEssenceCost() const
|
||||
{
|
||||
return hasProperty(EssenceProperties::Cost) ? getProperty(EssenceProperties::Cost)
|
||||
: getProperty(Mtg::ManaCost);
|
||||
}
|
||||
QString CardInfo::getCmc() const
|
||||
{
|
||||
return getProperty(Mtg::ConvertedManaCost);
|
||||
return getEssenceValue();
|
||||
}
|
||||
QString CardInfo::getColors() const
|
||||
{
|
||||
return getProperty(Mtg::Colors);
|
||||
return getEssenceTypes();
|
||||
}
|
||||
void CardInfo::setColors(const QString &value)
|
||||
{
|
||||
setProperty(Mtg::Colors, value);
|
||||
setProperty(EssenceProperties::Types, Essence::normalizeIdentity(value));
|
||||
}
|
||||
QString CardInfo::getLoyalty() const
|
||||
{
|
||||
|
|
@ -326,11 +344,53 @@ QString CardInfo::getMainCardType() const
|
|||
}
|
||||
QString CardInfo::getManaCost() const
|
||||
{
|
||||
return getProperty(Mtg::ManaCost);
|
||||
return getEssenceCost();
|
||||
}
|
||||
QString CardInfo::getIdentity() const
|
||||
{
|
||||
return hasProperty(ContinuumCardProperties::Identity) ? getProperty(ContinuumCardProperties::Identity)
|
||||
: getProperty(ContinuumCardProperties::Alias);
|
||||
}
|
||||
QString CardInfo::getAttack() const
|
||||
{
|
||||
if (hasProperty(ContinuumCardProperties::Attack)) {
|
||||
return getProperty(ContinuumCardProperties::Attack);
|
||||
}
|
||||
if (hasProperty(ContinuumCardProperties::Strength)) {
|
||||
return getProperty(ContinuumCardProperties::Strength);
|
||||
}
|
||||
const QString legacyStats = getProperty(Mtg::PowTough);
|
||||
const qsizetype slash = legacyStats.indexOf(QLatin1Char('/'));
|
||||
return slash < 0 ? QString() : legacyStats.left(slash);
|
||||
}
|
||||
QString CardInfo::getArmour() const
|
||||
{
|
||||
return hasProperty(ContinuumCardProperties::Armour) ? getProperty(ContinuumCardProperties::Armour)
|
||||
: getProperty(ContinuumCardProperties::Armor);
|
||||
}
|
||||
QString CardInfo::getHealth() const
|
||||
{
|
||||
if (hasProperty(ContinuumCardProperties::Health)) {
|
||||
return getProperty(ContinuumCardProperties::Health);
|
||||
}
|
||||
const QString legacyStats = getProperty(Mtg::PowTough);
|
||||
const qsizetype slash = legacyStats.indexOf(QLatin1Char('/'));
|
||||
return slash < 0 ? QString() : legacyStats.mid(slash + 1);
|
||||
}
|
||||
QString CardInfo::getAtkHp() const
|
||||
{
|
||||
const QString attack = getAttack();
|
||||
const QString health = getHealth();
|
||||
if (attack.isEmpty() && health.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return (attack.isEmpty() ? QStringLiteral("?") : attack) + QLatin1Char('/') +
|
||||
(health.isEmpty() ? QStringLiteral("?") : health);
|
||||
}
|
||||
QString CardInfo::getPowTough() const
|
||||
{
|
||||
return getProperty(Mtg::PowTough);
|
||||
// Compatibility accessor. Display terminology is always ATK/HP.
|
||||
return getAtkHp();
|
||||
}
|
||||
void CardInfo::setPowTough(const QString &value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -466,12 +466,24 @@ public:
|
|||
/** @name Legacy/Convenience Property Accessors */ //@{
|
||||
[[nodiscard]] QString getCardType() const;
|
||||
void setCardType(const QString &value);
|
||||
// Native Continuumatrice card properties; older MTG getter names below
|
||||
// remain compatibility wrappers used by the inherited model/analytics API.
|
||||
[[nodiscard]] QString getEssenceCost() const;
|
||||
[[nodiscard]] QString getEssenceTypes() const;
|
||||
[[nodiscard]] QString getEssenceValue() const;
|
||||
[[nodiscard]] QString getCmc() const;
|
||||
[[nodiscard]] QString getColors() const;
|
||||
void setColors(const QString &value);
|
||||
[[nodiscard]] QString getLoyalty() const;
|
||||
[[nodiscard]] QString getMainCardType() const;
|
||||
[[nodiscard]] QString getManaCost() const;
|
||||
// Native static card-definition stats and identity. The database exporter
|
||||
// writes identity/attack/armour/health, while older XML keys are readable.
|
||||
[[nodiscard]] QString getIdentity() const;
|
||||
[[nodiscard]] QString getAttack() const;
|
||||
[[nodiscard]] QString getArmour() const;
|
||||
[[nodiscard]] QString getHealth() const;
|
||||
[[nodiscard]] QString getAtkHp() const;
|
||||
[[nodiscard]] QString getPowTough() const;
|
||||
void setPowTough(const QString &value);
|
||||
//@}
|
||||
|
|
|
|||
|
|
@ -319,7 +319,11 @@ void CardDatabase::swapInDatabaseData(CardDatabaseData data)
|
|||
sets = std::move(data.sets);
|
||||
formats = std::move(data.formats);
|
||||
|
||||
loadStatus = cards.isEmpty() ? NotLoaded : Ok;
|
||||
// A successfully parsed card database may legitimately contain no cards.
|
||||
// Continuumatrice starts with an intentionally empty catalogue, which is
|
||||
// still LOADED. NotLoaded is reserved for before loading and clear().
|
||||
// The UI uses this status to reveal its filters and empty results view.
|
||||
loadStatus = Ok;
|
||||
|
||||
// inform listeners that the whole database was replaced; they should
|
||||
// rebuild from the live containers in a single batch instead of reacting
|
||||
|
|
|
|||
76
libcockatrice_card/libcockatrice/card/essence_types.h
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#ifndef CONTINUUMATRICE_ESSENCE_TYPES_H
|
||||
#define CONTINUUMATRICE_ESSENCE_TYPES_H
|
||||
|
||||
#include <QChar>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatchIterator>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace Essence
|
||||
{
|
||||
// Canonical Continuum Universes CCG essence symbols. N is colorless.
|
||||
// The other codes have no names, colors or game mechanics assigned here.
|
||||
inline const QString &symbols()
|
||||
{
|
||||
static const QString value = QStringLiteral("MSOGEAQCJBRLVTPN");
|
||||
return value;
|
||||
}
|
||||
|
||||
inline bool isSymbol(const QChar symbol)
|
||||
{
|
||||
return symbols().contains(symbol);
|
||||
}
|
||||
|
||||
// Stable ordering for the card's explicitly recorded Essence identity. This
|
||||
// is a list of distinct codes, NOT a derived resource cost or mana color.
|
||||
inline QString normalizeIdentity(const QString &input)
|
||||
{
|
||||
const QString uppercase = input.toUpper();
|
||||
QString normalized;
|
||||
for (const QChar code : symbols()) {
|
||||
if (uppercase.contains(code)) {
|
||||
normalized.append(code);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Costs may contain bare or braced symbols, e.g. MSN or {M}{S}{N}.
|
||||
// Decimal quantities are passed through as generic amounts for display.
|
||||
// Hybrid/phrexian/other MTG-specific syntax is NOT defined as Essence.
|
||||
inline QStringList parseCost(const QString &cost)
|
||||
{
|
||||
static const QRegularExpression regex(QStringLiteral(R"(\{([^}]+)\}|(\d+)|([MSOGEAQCJBRLVTPN]))"));
|
||||
QStringList result;
|
||||
auto matches = regex.globalMatch(cost);
|
||||
while (matches.hasNext()) {
|
||||
const auto match = matches.next();
|
||||
if (!match.captured(1).isEmpty()) {
|
||||
const QString token = match.captured(1);
|
||||
if (token.size() == 1 && isSymbol(token.at(0))) {
|
||||
result.append(token);
|
||||
} else {
|
||||
// Numeric quantities may also be written in braces: {2}{N}.
|
||||
bool isNumber = !token.isEmpty();
|
||||
for (const QChar ch : token) {
|
||||
if (!ch.isDigit()) {
|
||||
isNumber = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isNumber) {
|
||||
result.append(token);
|
||||
}
|
||||
}
|
||||
} else if (!match.captured(2).isEmpty()) {
|
||||
result.append(match.captured(2));
|
||||
} else if (!match.captured(3).isEmpty()) {
|
||||
result.append(match.captured(3));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace Essence
|
||||
|
||||
#endif // CONTINUUMATRICE_ESSENCE_TYPES_H
|
||||
|
|
@ -16,6 +16,26 @@
|
|||
* Note: Mtg = "Maybe that game"
|
||||
*/
|
||||
|
||||
// These remain only for reading older Cockatrice XML. New cards use Essence.
|
||||
namespace EssenceProperties
|
||||
{
|
||||
inline const QString Cost = QStringLiteral("essencecost");
|
||||
inline const QString Types = QStringLiteral("essence");
|
||||
inline const QString Value = QStringLiteral("essencevalue");
|
||||
}
|
||||
|
||||
// Continuum Universes CCG card-definition fields, not mutable match stats.
|
||||
namespace ContinuumCardProperties
|
||||
{
|
||||
inline const QString Identity = QStringLiteral("identity");
|
||||
inline const QString Alias = QStringLiteral("alias"); // Earlier exports may use this name.
|
||||
inline const QString Attack = QStringLiteral("attack");
|
||||
inline const QString Strength = QStringLiteral("strength"); // Legacy MSE field ID.
|
||||
inline const QString Armour = QStringLiteral("armour");
|
||||
inline const QString Armor = QStringLiteral("armor"); // Legacy American spelling.
|
||||
inline const QString Health = QStringLiteral("health");
|
||||
}
|
||||
|
||||
namespace Mtg
|
||||
{
|
||||
QString const CardType("type");
|
||||
|
|
@ -31,26 +51,38 @@ QString const ColorIdentity("coloridentity");
|
|||
|
||||
inline static const QString getNicePropertyName(QString key)
|
||||
{
|
||||
if (key == EssenceProperties::Cost || key == ManaCost) {
|
||||
return QCoreApplication::translate("Essence", "Essence Cost");
|
||||
}
|
||||
if (key == EssenceProperties::Types || key == Colors || key == ColorIdentity) {
|
||||
return QCoreApplication::translate("Essence", "Essence");
|
||||
}
|
||||
if (key == EssenceProperties::Value || key == ConvertedManaCost) {
|
||||
return QCoreApplication::translate("Essence", "Essence Value");
|
||||
}
|
||||
if (key == ContinuumCardProperties::Identity || key == ContinuumCardProperties::Alias) {
|
||||
return QCoreApplication::translate("Continuum", "Identity");
|
||||
}
|
||||
if (key == ContinuumCardProperties::Attack || key == ContinuumCardProperties::Strength) {
|
||||
return QCoreApplication::translate("Continuum", "Attack");
|
||||
}
|
||||
if (key == ContinuumCardProperties::Armour || key == ContinuumCardProperties::Armor) {
|
||||
return QCoreApplication::translate("Continuum", "Armour");
|
||||
}
|
||||
if (key == ContinuumCardProperties::Health) {
|
||||
return QCoreApplication::translate("Continuum", "Health");
|
||||
}
|
||||
if (key == CardType) {
|
||||
return QCoreApplication::translate("Mtg", "Card Type");
|
||||
}
|
||||
if (key == ConvertedManaCost) {
|
||||
return QCoreApplication::translate("Mtg", "Mana Value");
|
||||
}
|
||||
if (key == Colors) {
|
||||
return QCoreApplication::translate("Mtg", "Color(s)");
|
||||
}
|
||||
if (key == Loyalty) {
|
||||
return QCoreApplication::translate("Mtg", "Loyalty");
|
||||
}
|
||||
if (key == MainCardType) {
|
||||
return QCoreApplication::translate("Mtg", "Main Card Type");
|
||||
}
|
||||
if (key == ManaCost) {
|
||||
return QCoreApplication::translate("Mtg", "Mana Cost");
|
||||
}
|
||||
if (key == PowTough) {
|
||||
return QCoreApplication::translate("Mtg", "P/T");
|
||||
return QCoreApplication::translate("Continuum", "ATK/HP");
|
||||
}
|
||||
if (key == Side) {
|
||||
return QCoreApplication::translate("Mtg", "Side");
|
||||
|
|
@ -58,9 +90,6 @@ inline static const QString getNicePropertyName(QString key)
|
|||
if (key == Layout) {
|
||||
return QCoreApplication::translate("Mtg", "Layout");
|
||||
}
|
||||
if (key == ColorIdentity) {
|
||||
return QCoreApplication::translate("Mtg", "Color Identity");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
} // namespace Mtg
|
||||
|
|
|
|||
|
|
@ -68,15 +68,15 @@ const QString CardFilter::attrName(Attr a)
|
|||
case AttrType:
|
||||
return tr("Type");
|
||||
case AttrColor:
|
||||
return tr("Color");
|
||||
return tr("Essence");
|
||||
case AttrText:
|
||||
return tr("Text");
|
||||
case AttrSet:
|
||||
return tr("Set");
|
||||
case AttrManaCost:
|
||||
return tr("Mana Cost");
|
||||
return tr("Essence Cost");
|
||||
case AttrCmc:
|
||||
return tr("Mana Value");
|
||||
return tr("Essence Value");
|
||||
case AttrRarity:
|
||||
return tr("Rarity");
|
||||
case AttrPow:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "filter_card.h"
|
||||
#include "libcockatrice/card/card_localization.h"
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
#include "libcockatrice/card/set/card_set.h"
|
||||
|
||||
|
|
@ -177,23 +178,16 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st
|
|||
|
||||
switch (attr()) {
|
||||
case CardFilter::AttrColor: {
|
||||
QString converted = term.trimmed();
|
||||
|
||||
converted.replace("green", "g", Qt::CaseInsensitive);
|
||||
converted.replace("grn", "g", Qt::CaseInsensitive);
|
||||
converted.replace("blue", "u", Qt::CaseInsensitive);
|
||||
converted.replace("blu", "u", Qt::CaseInsensitive);
|
||||
converted.replace("black", "b", Qt::CaseInsensitive);
|
||||
converted.replace("blk", "b", Qt::CaseInsensitive);
|
||||
converted.replace("red", "r", Qt::CaseInsensitive);
|
||||
converted.replace("white", "w", Qt::CaseInsensitive);
|
||||
converted.replace("wht", "w", Qt::CaseInsensitive);
|
||||
converted.replace("colorless", "c", Qt::CaseInsensitive);
|
||||
converted.replace("colourless", "c", Qt::CaseInsensitive);
|
||||
converted.replace("none", "c", Qt::CaseInsensitive);
|
||||
|
||||
converted.replace(QString(" "), QString(""), Qt::CaseInsensitive);
|
||||
|
||||
// The old MTG color-name conversions are invalid for Essence.
|
||||
// N is explicitly colorless; an unspecified identity is not N.
|
||||
QString converted = term.trimmed().toUpper();
|
||||
if (converted == QStringLiteral("COLORLESS") || converted == QStringLiteral("COLOURLESS")) {
|
||||
converted = QStringLiteral("N");
|
||||
}
|
||||
converted.remove(QLatin1Char(' '));
|
||||
converted.remove(QLatin1Char(','));
|
||||
converted.remove(QLatin1Char('{'));
|
||||
converted.remove(QLatin1Char('}'));
|
||||
convertedColorTerm = converted;
|
||||
break;
|
||||
}
|
||||
|
|
@ -240,8 +234,7 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st
|
|||
break;
|
||||
}
|
||||
case CardFilter::AttrManaCost:
|
||||
sortedManaCostTerm = term.toUpper();
|
||||
// Sort the mana cost so it will be easy to find
|
||||
sortedManaCostTerm = Essence::parseCost(term.toUpper()).join(QString());
|
||||
std::sort(sortedManaCostTerm.begin(), sortedManaCostTerm.end());
|
||||
break;
|
||||
case CardFilter::AttrLoyalty:
|
||||
|
|
@ -360,25 +353,16 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const
|
|||
|
||||
bool FilterItem::acceptColor(const CardInfoPtr &info) const
|
||||
{
|
||||
const QString colors = info->getColors();
|
||||
|
||||
// Colorless card filter
|
||||
if (convertedColorTerm.toLower() == "c" && colors.length() < 1) {
|
||||
return true;
|
||||
if (convertedColorTerm.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a tricky part, if the filter has multiple colors in it, like UGW,
|
||||
* then we should match all of them to the card's colors
|
||||
*/
|
||||
int match_count = 0;
|
||||
for (const QChar &color : convertedColorTerm) {
|
||||
if (colors.contains(color, Qt::CaseInsensitive)) {
|
||||
match_count++;
|
||||
const QString essence = info->getEssenceTypes();
|
||||
for (const QChar code : convertedColorTerm) {
|
||||
if (!Essence::isSymbol(code) || !essence.contains(code)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return match_count == convertedColorTerm.length();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const
|
||||
|
|
@ -421,14 +405,13 @@ bool FilterItem::acceptSet(const CardInfoPtr &info) const
|
|||
|
||||
bool FilterItem::acceptManaCost(const CardInfoPtr &info) const
|
||||
{
|
||||
// Try to seperate the mana cost in case it's a split card
|
||||
// if it's not a split card the loop will run only once
|
||||
for (const QString &manaCostPart : info->getManaCost().split("//")) {
|
||||
QString fullManaCost = manaCostPart;
|
||||
std::sort(fullManaCost.begin(), fullManaCost.end());
|
||||
|
||||
// If the partial is found in the full, return true
|
||||
if (fullManaCost.contains(sortedManaCostTerm)) {
|
||||
if (sortedManaCostTerm.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (const QString &costPart : info->getManaCost().split(QStringLiteral("//"))) {
|
||||
QString sortedCost = Essence::parseCost(costPart).join(QString());
|
||||
std::sort(sortedCost.begin(), sortedCost.end());
|
||||
if (sortedCost.contains(sortedManaCostTerm)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,11 +70,18 @@ bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelInd
|
|||
if (isRightType && (!isLeftType || rightString.size() == cardName.size())) {
|
||||
return false;
|
||||
}
|
||||
} else if (right.column() == CardDatabaseModel::PTColumn && left.column() == CardDatabaseModel::PTColumn) {
|
||||
// The comparator runs O(n log n) times while the view is sorted, so
|
||||
// splitting every power/toughness string into a QStringList allocates
|
||||
// a lot for data that has exactly one separator. Work on views and
|
||||
// only split the two halves out when both strings are well formed.
|
||||
} else if (left.column() == right.column() &&
|
||||
(left.column() == CardDatabaseModel::AttackColumn ||
|
||||
left.column() == CardDatabaseModel::ArmourColumn ||
|
||||
left.column() == CardDatabaseModel::HealthColumn)) {
|
||||
// Compare numeric card statistics as numbers: 9 sorts before 10.
|
||||
// Values containing expressions retain the existing numeric-prefix
|
||||
// comparator instead of being forced into an integer type.
|
||||
return lessThanNumerically(leftString, rightString) < 0;
|
||||
} else if (right.column() == CardDatabaseModel::AtkHpColumn &&
|
||||
left.column() == CardDatabaseModel::AtkHpColumn) {
|
||||
// ATK/HP sorts by Attack first, then Health, both numerically.
|
||||
// Operate on views to avoid allocations during large database sorts.
|
||||
const QStringView leftPT = leftString;
|
||||
const QStringView rightPT = rightString;
|
||||
const qsizetype leftSlash = leftPT.indexOf(u'/');
|
||||
|
|
@ -83,11 +90,11 @@ bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelInd
|
|||
const bool rightIsWellFormed = rightSlash >= 0 && rightPT.indexOf(u'/', rightSlash + 1) < 0;
|
||||
|
||||
if (leftIsWellFormed && rightIsWellFormed) {
|
||||
const int powerComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash));
|
||||
if (powerComparison != 0) {
|
||||
return powerComparison < 0;
|
||||
const int attackComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash));
|
||||
if (attackComparison != 0) {
|
||||
return attackComparison < 0;
|
||||
}
|
||||
// power equal, check toughness
|
||||
// Attack equal, compare Health
|
||||
return lessThanNumerically(leftPT.mid(leftSlash + 1), rightPT.mid(rightSlash + 1)) < 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
class QObject;
|
||||
|
||||
#define CARDDBMODEL_COLUMNS 6
|
||||
|
||||
CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent)
|
||||
: QAbstractListModel(parent), db(_db), showOnlyCardsFromEnabledSets(_showOnlyCardsFromEnabledSets)
|
||||
|
|
@ -38,12 +37,12 @@ int CardDatabaseModel::columnCount(const QModelIndex &parent) const
|
|||
{
|
||||
// Card rows are leaves, so a valid parent holds no columns. Reporting the
|
||||
// row width for a card index would claim child cells that do not exist.
|
||||
return parent.isValid() ? 0 : CARDDBMODEL_COLUMNS;
|
||||
return parent.isValid() ? 0 : ColumnCount;
|
||||
}
|
||||
|
||||
QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= cardList.size() || index.column() >= CARDDBMODEL_COLUMNS ||
|
||||
if (!index.isValid() || index.row() >= cardList.size() || index.column() >= ColumnCount ||
|
||||
(role != Qt::DisplayRole && role != SortRole)) {
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -59,10 +58,18 @@ QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const
|
|||
: card->getManaCost();
|
||||
case CardTypeColumn:
|
||||
return card->getCardType();
|
||||
case PTColumn:
|
||||
return card->getPowTough();
|
||||
case AtkHpColumn:
|
||||
return card->getAtkHp();
|
||||
case ColorColumn:
|
||||
return card->getColors();
|
||||
case IdentityColumn:
|
||||
return card->getIdentity();
|
||||
case AttackColumn:
|
||||
return card->getAttack();
|
||||
case ArmourColumn:
|
||||
return card->getArmour();
|
||||
case HealthColumn:
|
||||
return card->getHealth();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -82,13 +89,21 @@ QVariant CardDatabaseModel::headerData(int section, Qt::Orientation orientation,
|
|||
case SetListColumn:
|
||||
return QString(tr("Sets"));
|
||||
case ManaCostColumn:
|
||||
return QString(tr("Mana cost"));
|
||||
return QString(tr("Essence Cost"));
|
||||
case CardTypeColumn:
|
||||
return QString(tr("Card type"));
|
||||
case PTColumn:
|
||||
return QString(tr("P/T"));
|
||||
case AtkHpColumn:
|
||||
return QString(tr("ATK/HP"));
|
||||
case ColorColumn:
|
||||
return QString(tr("Color(s)"));
|
||||
return QString(tr("Essence"));
|
||||
case IdentityColumn:
|
||||
return QString(tr("Identity"));
|
||||
case AttackColumn:
|
||||
return QString(tr("Attack"));
|
||||
case ArmourColumn:
|
||||
return QString(tr("Armour"));
|
||||
case HealthColumn:
|
||||
return QString(tr("Health"));
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -101,7 +116,7 @@ void CardDatabaseModel::cardInfoChanged(const CardInfoPtr &card)
|
|||
return;
|
||||
}
|
||||
|
||||
emit dataChanged(index(row, 0), index(row, CARDDBMODEL_COLUMNS - 1));
|
||||
emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
|
||||
}
|
||||
|
||||
bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card) const
|
||||
|
|
|
|||
|
|
@ -28,9 +28,15 @@ public:
|
|||
NameColumn,
|
||||
SetListColumn,
|
||||
ManaCostColumn,
|
||||
PTColumn,
|
||||
AtkHpColumn, // Formerly P/T; keep column index for saved table layouts.
|
||||
CardTypeColumn,
|
||||
ColorColumn
|
||||
ColorColumn,
|
||||
IdentityColumn,
|
||||
AttackColumn,
|
||||
ArmourColumn,
|
||||
HealthColumn,
|
||||
ColumnCount,
|
||||
PTColumn = AtkHpColumn // Compatibility alias for inherited callers.
|
||||
};
|
||||
enum Role
|
||||
{
|
||||
|
|
|
|||
|
|
@ -172,6 +172,13 @@ endfunction()
|
|||
# --- Unit tests ------------------------------------------------------------------
|
||||
|
||||
cockatrice_add_test(NAME expression_test SOURCES expression_test.cpp LIBS libcockatrice_utility)
|
||||
# XML v3/v4 exporters in libcockatrice_card reference VERSION_STRING.
|
||||
# Link the same generated version implementation used by the client/server.
|
||||
cockatrice_add_test(
|
||||
NAME essence_types_test
|
||||
SOURCES ${VERSION_STRING_CPP} essence_types_test.cpp
|
||||
LIBS libcockatrice_card
|
||||
)
|
||||
cockatrice_add_test(NAME clamped_arithmetic_test SOURCES clamped_arithmetic_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_data.h>
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
#include <libcockatrice/card/game_specific_terms.h>
|
||||
#include <libcockatrice/card/lazy_properties_hash.h>
|
||||
#include <libcockatrice/card/printing/printing_info.h>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
|
|
@ -35,6 +36,7 @@
|
|||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
|
@ -65,6 +67,65 @@ TEST(CardDatabaseTest, LoadXml)
|
|||
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
|
||||
}
|
||||
|
||||
TEST(CardDatabaseTest, EmptyLoadedSnapshotIsReadyNotLoading)
|
||||
{
|
||||
NoopCardPreferenceProvider prefs;
|
||||
TestCardDatabasePathProvider pathProvider;
|
||||
NoopCardSetPriorityController controller;
|
||||
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
|
||||
|
||||
EXPECT_EQ(NotLoaded, db.getLoadStatus());
|
||||
|
||||
// A newly bootstrapped Continuumatrice profile contains a valid, but empty,
|
||||
// XML v4 catalogue. Successful swap must not trap the visual browser on
|
||||
// "Loading database..." when there are zero cards.
|
||||
CardDatabaseData emptySnapshot;
|
||||
db.swapInDatabaseData(std::move(emptySnapshot));
|
||||
EXPECT_TRUE(db.getCardList().isEmpty());
|
||||
EXPECT_EQ(Ok, db.getLoadStatus());
|
||||
|
||||
db.clear();
|
||||
EXPECT_EQ(NotLoaded, db.getLoadStatus());
|
||||
}
|
||||
|
||||
TEST(CardInfoTest, ContinuumIdentityAndStatsHaveNativeAndLegacyPropertyAccess)
|
||||
{
|
||||
auto native = CardInfo::newInstance("Native Continuum Card");
|
||||
native->setProperty("identity", "The Shadow Queen");
|
||||
native->setProperty("attack", "10");
|
||||
native->setProperty("armour", "3");
|
||||
native->setProperty("health", "12");
|
||||
|
||||
EXPECT_EQ(native->getIdentity(), "The Shadow Queen");
|
||||
EXPECT_EQ(native->getAttack(), "10");
|
||||
EXPECT_EQ(native->getArmour(), "3");
|
||||
EXPECT_EQ(native->getHealth(), "12");
|
||||
EXPECT_EQ(native->getAtkHp(), "10/12");
|
||||
EXPECT_EQ(native->getPowTough(), "10/12"); // Legacy accessor remains useful.
|
||||
|
||||
auto legacyMse = CardInfo::newInstance("Legacy Continuum Card");
|
||||
legacyMse->setProperty("alias", "The Guardian");
|
||||
legacyMse->setProperty("strength", "9");
|
||||
legacyMse->setProperty("armor", "4");
|
||||
legacyMse->setProperty("health", "8");
|
||||
EXPECT_EQ(legacyMse->getIdentity(), "The Guardian");
|
||||
EXPECT_EQ(legacyMse->getAttack(), "9");
|
||||
EXPECT_EQ(legacyMse->getArmour(), "4");
|
||||
EXPECT_EQ(legacyMse->getHealth(), "8");
|
||||
EXPECT_EQ(legacyMse->getAtkHp(), "9/8");
|
||||
|
||||
auto oldPt = CardInfo::newInstance("Old Imported Card");
|
||||
oldPt->setProperty(Mtg::PowTough, "2/7");
|
||||
EXPECT_EQ(oldPt->getAttack(), "2");
|
||||
EXPECT_EQ(oldPt->getHealth(), "7");
|
||||
EXPECT_EQ(oldPt->getAtkHp(), "2/7");
|
||||
|
||||
// Missing values should not be represented as false zeroes.
|
||||
auto blank = CardInfo::newInstance("No Stats");
|
||||
EXPECT_TRUE(blank->getAtkHp().isEmpty());
|
||||
EXPECT_TRUE(blank->getArmour().isEmpty());
|
||||
}
|
||||
|
||||
TEST(CardDatabaseTest, LazyPropertiesHashStreamsBlobAccessors)
|
||||
{
|
||||
QHash<QString, QString> props;
|
||||
|
|
|
|||
60
tests/essence_types_test.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
TEST(EssenceTypesTest, HasExactlySixteenCanonicalSymbols)
|
||||
{
|
||||
EXPECT_EQ(Essence::symbols(), QStringLiteral("MSOGEAQCJBRLVTPN"));
|
||||
EXPECT_EQ(Essence::symbols().size(), 16);
|
||||
EXPECT_TRUE(Essence::isSymbol(QChar('N'))); // N is colorless.
|
||||
EXPECT_TRUE(Essence::isSymbol(QChar('J'))); // J is Jasper, between C and B.
|
||||
EXPECT_FALSE(Essence::isSymbol(QChar('W'))); // MTG white is not Essence.
|
||||
EXPECT_FALSE(Essence::isSymbol(QChar('U'))); // MTG blue is not Essence.
|
||||
}
|
||||
|
||||
TEST(EssenceTypesTest, ParsesAllBracedAndUnbracedTypes)
|
||||
{
|
||||
const QString raw = QStringLiteral("{M}{S}{O}{G}{E}{A}{Q}{C}{J}{B}{R}{L}{V}{T}{P}{N}");
|
||||
EXPECT_EQ(Essence::parseCost(raw).join(QString()), Essence::symbols());
|
||||
EXPECT_EQ(Essence::parseCost(QStringLiteral("MSN")).join(QString()), QStringLiteral("MSN"));
|
||||
EXPECT_EQ(Essence::parseCost(QStringLiteral("{C}{J}{B}{N}")).join(QString()), QStringLiteral("CJBN"));
|
||||
EXPECT_EQ(Essence::parseCost(QStringLiteral("{2}{N}{M}")).join(QStringLiteral("|")),
|
||||
QStringLiteral("2|N|M"));
|
||||
}
|
||||
|
||||
TEST(EssenceTypesTest, RejectsUnknownLegacyAndHybridCodes)
|
||||
{
|
||||
EXPECT_TRUE(Essence::parseCost(QStringLiteral("{W}{U}{Z}{M/N}")).isEmpty());
|
||||
EXPECT_EQ(Essence::parseCost(QStringLiteral("{N}")), QStringList{QStringLiteral("N")});
|
||||
}
|
||||
|
||||
TEST(EssenceTypesTest, NormalizesUniqueIdentityInCanonicalOrder)
|
||||
{
|
||||
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("NSSMMO")), QStringLiteral("MSON"));
|
||||
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("N")), QStringLiteral("N"));
|
||||
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("BJNJC")), QStringLiteral("CJBN"));
|
||||
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("WU")), QString());
|
||||
}
|
||||
|
||||
TEST(EssenceTypesTest, NativePropertiesTakePrecedenceOverLegacyFallbacks)
|
||||
{
|
||||
const QHash<QString, QString> props = {
|
||||
{QStringLiteral("essencecost"), QStringLiteral("{M}{N}")},
|
||||
{QStringLiteral("essence"), QStringLiteral("NM")},
|
||||
{QStringLiteral("essencevalue"), QStringLiteral("2")},
|
||||
{QStringLiteral("manacost"), QStringLiteral("{W}{U}")},
|
||||
{QStringLiteral("colors"), QStringLiteral("WU")},
|
||||
{QStringLiteral("cmc"), QStringLiteral("9")}
|
||||
};
|
||||
|
||||
const CardInfoPtr card = CardInfo::newInstance(QStringLiteral("Essence Test"),
|
||||
QString(), false, props, {}, {}, {}, {});
|
||||
ASSERT_TRUE(card);
|
||||
EXPECT_EQ(card->getManaCost(), QStringLiteral("{M}{N}"));
|
||||
EXPECT_EQ(card->getEssenceTypes(), QStringLiteral("MN"));
|
||||
EXPECT_EQ(card->getColors(), QStringLiteral("MN"));
|
||||
EXPECT_EQ(card->getCmc(), QStringLiteral("2"));
|
||||
}
|
||||
|
|
@ -103,6 +103,54 @@ TEST(ModelContract, CardDatabaseModelAndDisplayProxyKeepContract)
|
|||
EXPECT_LT(display.rowCount(), source.rowCount());
|
||||
}
|
||||
|
||||
TEST(ModelContract, ContinuumCardStatsAreSortableAndLabeled)
|
||||
{
|
||||
NoopCardPreferenceProvider prefs;
|
||||
TestCardDatabasePathProvider paths;
|
||||
NoopCardSetPriorityController setPriority;
|
||||
CardDatabase database(nullptr, &prefs, &paths, &setPriority);
|
||||
|
||||
auto swift = CardInfo::newInstance("Swift");
|
||||
swift->setProperty("identity", "Astra");
|
||||
swift->setProperty("attack", "9");
|
||||
swift->setProperty("armour", "10");
|
||||
swift->setProperty("health", "2");
|
||||
|
||||
auto sturdy = CardInfo::newInstance("Sturdy");
|
||||
sturdy->setProperty("identity", "Zephyr");
|
||||
sturdy->setProperty("attack", "10");
|
||||
sturdy->setProperty("armour", "2");
|
||||
sturdy->setProperty("health", "12");
|
||||
|
||||
database.addCard(swift);
|
||||
database.addCard(sturdy);
|
||||
|
||||
CardDatabaseModel source(&database, false);
|
||||
CardDatabaseDisplayModel display;
|
||||
display.setSourceModel(&source);
|
||||
|
||||
ASSERT_EQ(source.columnCount(), CardDatabaseModel::ColumnCount);
|
||||
ASSERT_EQ(display.rowCount(), 2);
|
||||
EXPECT_EQ(source.headerData(CardDatabaseModel::AtkHpColumn, Qt::Horizontal).toString(), "ATK/HP");
|
||||
EXPECT_EQ(source.headerData(CardDatabaseModel::IdentityColumn, Qt::Horizontal).toString(), "Identity");
|
||||
EXPECT_EQ(source.headerData(CardDatabaseModel::AttackColumn, Qt::Horizontal).toString(), "Attack");
|
||||
EXPECT_EQ(source.headerData(CardDatabaseModel::ArmourColumn, Qt::Horizontal).toString(), "Armour");
|
||||
EXPECT_EQ(source.headerData(CardDatabaseModel::HealthColumn, Qt::Horizontal).toString(), "Health");
|
||||
|
||||
const auto firstName = [&display] { return display.index(0, CardDatabaseModel::NameColumn).data().toString(); };
|
||||
|
||||
display.sort(CardDatabaseModel::AttackColumn, Qt::AscendingOrder);
|
||||
EXPECT_EQ(firstName(), "Swift") << "9 ATK should precede 10 ATK";
|
||||
display.sort(CardDatabaseModel::ArmourColumn, Qt::AscendingOrder);
|
||||
EXPECT_EQ(firstName(), "Sturdy") << "2 Armour should precede 10 Armour";
|
||||
display.sort(CardDatabaseModel::HealthColumn, Qt::AscendingOrder);
|
||||
EXPECT_EQ(firstName(), "Swift") << "2 HP should precede 12 HP";
|
||||
display.sort(CardDatabaseModel::AtkHpColumn, Qt::AscendingOrder);
|
||||
EXPECT_EQ(firstName(), "Swift") << "ATK/HP should sort attack and health numerically";
|
||||
display.sort(CardDatabaseModel::IdentityColumn, Qt::DescendingOrder);
|
||||
EXPECT_EQ(firstName(), "Sturdy") << "Identity should sort text from the alias/identity property";
|
||||
}
|
||||
|
||||
TEST(ModelContract, SetsModelKeepsContract)
|
||||
{
|
||||
CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(),
|
||||
|
|
|
|||