Merge pull request #5 from Continuum-Universes-Studio/feature/continuumatrice-essence

Continuumatrice CCG: 16 Essence symbols including Jasper J and colorless N
This commit is contained in:
Christian Daniel Jensen 2026-11-03 15:58:28 -07:00 • committed by GitHub
commit 2daf7f5da1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 2820 additions and 1733 deletions

View file

@ -157,6 +157,7 @@ set(cockatrice_SOURCES
src/interface/palette_editor/palette_editor_dialog.cpp src/interface/palette_editor/palette_editor_dialog.cpp
src/interface/widgets/cards/additional_info/color_identity_widget.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/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_cost_widget.cpp
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
src/interface/widgets/cards/art_crop_attribution.cpp src/interface/widgets/cards/art_crop_attribution.cpp

View file

@ -60,12 +60,22 @@
<file>resources/icons/update.png</file> <file>resources/icons/update.png</file>
<file>resources/icons/view.svg</file> <file>resources/icons/view.svg</file>
<file>resources/icons/mana/B.svg</file> <file>resources/icons/essence/M.svg</file>
<file>resources/icons/mana/C.svg</file> <file>resources/icons/essence/S.svg</file>
<file>resources/icons/mana/G.svg</file> <file>resources/icons/essence/O.svg</file>
<file>resources/icons/mana/R.svg</file> <file>resources/icons/essence/G.svg</file>
<file>resources/icons/mana/U.svg</file> <file>resources/icons/essence/E.svg</file>
<file>resources/icons/mana/W.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.jpg</file>
<file>resources/backgrounds/home-dark.jpg</file> <file>resources/backgrounds/home-dark.jpg</file>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 7.3 MiB

Before After
Before After

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -13,6 +13,7 @@
#include "libcockatrice/protocol/pb/event_server_shutdown.pb.h" #include "libcockatrice/protocol/pb/event_server_shutdown.pb.h"
#include <QChar> #include <QChar>
#include <QCoreApplication>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QLineEdit> #include <QLineEdit>
@ -217,7 +218,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
"games will be lost.\nReason for shutdown: %1", "games will be lost.\nReason for shutdown: %1",
"", event.minutes()) "", event.minutes())
.arg(QString::fromStdString(event.reason()))); .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.setText(tr("Scheduled server shutdown"));
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal); serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
serverShutdownMessageBox.setVisible(true); serverShutdownMessageBox.setVisible(true);
@ -333,7 +334,7 @@ void ConnectionController::onLoginError(int r,
case Response::RespContextError: { case Response::RespContextError: {
QMessageBox::critical(dialogParent, tr("Error"), 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 " "trying again.\nIf the error persists, ensure you are running the latest "
"version of the software and if needed contact the software developers.")); "version of the software and if needed contact the software developers."));
break; break;
@ -489,14 +490,14 @@ void ConnectionController::onProtocolVersionMismatch(int localVersion, int remot
{ {
if (localVersion > remoteVersion) { if (localVersion > remoteVersion) {
QMessageBox::critical(dialogParent, tr("Error"), 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" "version or connect to a suitable server.\n"
"Local version is %1, remote version is %2.") "Local version is %1, remote version is %2.")
.arg(localVersion) .arg(localVersion)
.arg(remoteVersion)); .arg(remoteVersion));
} else { } else {
QMessageBox::critical(dialogParent, tr("Error"), 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.") "Local version is %1, remote version is %2.")
.arg(localVersion) .arg(localVersion)
.arg(remoteVersion)); .arg(remoteVersion));
@ -631,7 +632,7 @@ void ConnectionController::onPromptForgotPasswordChallenge()
void ConnectionController::updateWindowTitle() void ConnectionController::updateWindowTitle()
{ {
const QString appName = QStringLiteral("Cockatrice"); const QString appName = QCoreApplication::applicationName();
QString title; QString title;
switch (remoteClient->getStatus()) { switch (remoteClient->getStatus()) {
@ -653,7 +654,7 @@ void ConnectionController::updateWindowTitle()
break; break;
} }
case StatusLoggedIn: { case StatusLoggedIn: {
title = remoteClient->getUserName() + "@" + remoteClient->peerName(); title = appName + " - " + remoteClient->getUserName() + "@" + remoteClient->peerName();
break; break;
} }
case StatusRequestingForgotPassword: case StatusRequestingForgotPassword:

View file

@ -251,9 +251,11 @@ void TableZone::paintBattlefieldRows(QPainter *painter)
*/ */
void TableZone::reloadBattlefieldAppearance() void TableZone::reloadBattlefieldAppearance()
{ {
frontZoneColor = QColor(142, 52, 61, 128); // Canonical default battlefield palette. Theme battlefield.ini can override
supportZoneColor = QColor(40, 105, 129, 128); // these with #RRGGBB (opaque) or #AARRGGBB (alpha) values.
commandZoneColor = QColor(94, 66, 140, 128); frontZoneColor = QColor(QStringLiteral("#dc143c"));
supportZoneColor = QColor(QStringLiteral("#c3b091"));
commandZoneColor = QColor(QStringLiteral("#0a0e3f"));
const QString themePath = themeManager->getCurrentThemePath(); const QString themePath = themeManager->getCurrentThemePath();
if (!themePath.isEmpty()) { if (!themePath.isEmpty()) {

View 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

View file

@ -488,7 +488,10 @@ const QPixmap &ManaSymbolPixmapGenerator::masterIcon(const QString &symbol)
return it.value(); 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(); QSize sourceSize = reader.size();
if (!sourceSize.isEmpty()) { if (!sourceSize.isEmpty()) {
sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio); sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio);

View file

@ -2,6 +2,7 @@
#include "../../../../client/settings/cache_settings.h" #include "../../../../client/settings/cache_settings.h"
#include "libcockatrice/card/card_info.h" #include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include "mana_symbol_widget.h" #include "mana_symbol_widget.h"
#include <QChar> #include <QChar>
@ -41,9 +42,9 @@ ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const CardInfoPtr &car
void ColorIdentityWidget::populateManaSymbolWidgets() void ColorIdentityWidget::populateManaSymbolWidgets()
{ {
// Define the full WUBRG set (White, Blue, Black, Red, Green) // Continuum Universes supports fourteen named Essence codes plus N (colorless).
QString fullColorIdentity = "WUBRG"; const QString &allEssences = Essence::symbols();
QStringList symbols = parseColorIdentity(colorIdentity); // Parse mana cost string QStringList symbols = parseColorIdentity(colorIdentity);
// clear old layout // clear old layout
QtUtils::clearLayoutRec(layout); QtUtils::clearLayoutRec(layout);
@ -55,7 +56,8 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
// populate mana symbols // populate mana symbols
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) { 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)); auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
layout->addWidget(manaSymbol); 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; QStringList symbols;
for (const QChar code : Essence::normalizeIdentity(essenceIdentity)) {
// Handle split costs (e.g., "3U // 4UU") symbols.append(QString(code));
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
}
}
} }
return symbols; return symbols;
} }

View file

@ -1,6 +1,7 @@
#include "deck_color_identity.h" #include "deck_color_identity.h"
#include "libcockatrice/card/card_info.h" #include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include <QChar> #include <QChar>
#include <QList> #include <QList>
@ -17,7 +18,7 @@ QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db
return {}; 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) { for (const QString &cardName : cardList) {
CardInfoPtr currentCard = db->getCardInfo(cardName); 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; QString colorIdentity;
const QString wubrgOrder = "WUBRG"; for (const QChar &color : Essence::symbols()) {
for (const QChar &color : wubrgOrder) {
if (colorSet.contains(color)) { if (colorSet.contains(color)) {
colorIdentity.append(color); colorIdentity.append(color);
} }

View file

@ -7,8 +7,9 @@ class CardDatabaseQuerier;
class DeckList; class DeckList;
/** /**
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color * @brief Computes the Essence identity of a deck from the defined Essence
* symbols of all cards in the main deck and sideboard, ordered WUBRG. * 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 * Shared as a free function so the deck storage previews and the deck share
* dialog compute identities identically. * dialog compute identities identically.

View file

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

View file

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

View file

@ -1,6 +1,7 @@
#include "mana_cost_widget.h" #include "mana_cost_widget.h"
#include "libcockatrice/card/card_info.h" #include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include "mana_symbol_widget.h" #include "mana_symbol_widget.h"
#include <QHBoxLayout> #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; return Essence::parseCost(essenceCost);
// 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;
} }

View file

@ -57,14 +57,14 @@ ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
void ManaBaseConfigDialog::retranslateUi() void ManaBaseConfigDialog::retranslateUi()
{ {
setWindowTitle(tr("Mana Base Configuration")); setWindowTitle(tr("Essence Production Configuration"));
displayTypeLabel->setText(tr("Display type:")); displayTypeLabel->setText(tr("Display type:"));
displayType->clear(); displayType->clear();
displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")}); 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::Ok)->setText(tr("OK"));
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));

View file

@ -1,5 +1,6 @@
#include "mana_base_widget.h" #include "mana_base_widget.h"
#include "../../../../essence_chart_colors.h"
#include "../../../general/display/charts/bars/bar_widget.h" #include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h" #include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h" #include "../../../general/display/charts/pies/color_pie.h"
@ -30,7 +31,7 @@ namespace
{ {
AnalyticsPanelWidgetRegistrar registerManaBase{ AnalyticsPanelWidgetRegistrar registerManaBase{
"manaBase", ManaBaseWidget::tr("Mana Base"), "manaBase", ManaBaseWidget::tr("Essence Production"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }}; [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
} // anonymous namespace } // anonymous namespace
@ -83,16 +84,12 @@ void ManaBaseWidget::updateDisplay()
// Choose display mode // Choose display mode
if (config.displayType == "bar") { if (config.displayType == "bar") {
const QList<QPair<QString, int>> sortedColors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(mapSorted); const QList<QPair<QString, int>> sortedColors = EssenceCharts::ordered(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)},
};
for (const auto &[color, count] : sortedColors) { for (const auto &[color, count] : sortedColors) {
QString label = QString("%1 %2 (%3)").arg(color).arg(count).arg(cardCount.value(color)); 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); barLayout->addWidget(bar);
} }

View file

@ -1,7 +1,7 @@
#include "mana_curve_category_widget.h" #include "mana_curve_category_widget.h"
#include "../../../general/display/charts/bars/bar_chart_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 "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config.h" #include "mana_curve_config.h"
@ -110,7 +110,7 @@ void ManaCurveCategoryWidget::updateDisplay(int minCmc,
QVector<BarSegment> segments; QVector<BarSegment> segments;
if (val > 0) { 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}); catBars.push_back({QString::number(cmc), segments});

View file

@ -64,7 +64,7 @@ void ManaCurveConfigDialog::retranslateUi()
{ {
labelGroupBy->setText(tr("Group By:")); labelGroupBy->setText(tr("Group By:"));
groupBy->setItemText(0, tr("type")); groupBy->setItemText(0, tr("type"));
groupBy->setItemText(1, tr("color")); groupBy->setItemText(1, tr("Essence"));
groupBy->setItemText(2, tr("subtype")); groupBy->setItemText(2, tr("subtype"));
groupBy->setItemText(3, tr("power")); groupBy->setItemText(3, tr("power"));
groupBy->setItemText(4, tr("toughness")); groupBy->setItemText(4, tr("toughness"));

View file

@ -1,7 +1,7 @@
#include "mana_curve_total_widget.h" #include "mana_curve_total_widget.h"
#include "../../../general/display/charts/bars/bar_chart_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 "mana_curve_config.h"
#include <QHBoxLayout> #include <QHBoxLayout>
@ -71,7 +71,7 @@ void ManaCurveTotalWidget::updateDisplay(const QString &categoryName,
cards = catIt->value(cmc); cards = catIt->value(cmc);
} }
segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)}); segments.push_back({category, value, cards, EssenceCharts::colorFor(category)});
} }
} }

View file

@ -23,7 +23,7 @@ namespace
{ {
AnalyticsPanelWidgetRegistrar registerManaCurve{ AnalyticsPanelWidgetRegistrar registerManaCurve{
"manaCurve", ManaCurveWidget::tr("Mana Curve"), "manaCurve", ManaCurveWidget::tr("Essence Curve"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }}; [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }};
} // anonymous namespace } // anonymous namespace

View file

@ -63,7 +63,7 @@ void ManaDevotionConfigDialog::retranslateUi()
displayType->setItemText(1, tr("bar")); displayType->setItemText(1, tr("bar"));
displayType->setItemText(2, tr("combinedBar")); displayType->setItemText(2, tr("combinedBar"));
labelFilters->setText(tr("Filter Colors (optional):")); labelFilters->setText(tr("Filter Essence types (optional):"));
} }
void ManaDevotionConfigDialog::accept() void ManaDevotionConfigDialog::accept()

View file

@ -1,5 +1,6 @@
#include "mana_devotion_widget.h" #include "mana_devotion_widget.h"
#include "../../../../essence_chart_colors.h"
#include "../../../general/display/charts/bars/bar_widget.h" #include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h" #include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h" #include "../../../general/display/charts/pies/color_pie.h"
@ -29,7 +30,7 @@ namespace
{ {
AnalyticsPanelWidgetRegistrar registerManaDevotion{ AnalyticsPanelWidgetRegistrar registerManaDevotion{
"manaDevotion", ManaDevotionWidget::tr("Mana Devotion"), "manaDevotion", ManaDevotionWidget::tr("Essence Cost Symbols"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }}; [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }};
} // anonymous namespace } // anonymous namespace
@ -82,18 +83,13 @@ void ManaDevotionWidget::updateDisplay()
mapSorted.insert(QString(it.key()), it.value()); 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 // Choose display mode
if (config.displayType == "bar") { if (config.displayType == "bar") {
// One BarWidget per devotion color // One BarWidget per devotion color
for (auto c : devoMap.keys()) { for (auto c : devoMap.keys()) {
QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c))); 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); barLayout->addWidget(bar);
} }

View file

@ -10,11 +10,18 @@
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <libcockatrice/card/essence_types.h>
#include <QVBoxLayout> #include <QVBoxLayout>
class QWidget; 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) ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
: QDialog(parent), analyzer(analyzer) : QDialog(parent), analyzer(analyzer)
@ -33,7 +40,7 @@ ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAna
filterList = new QListWidget(this); filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection); filterList->setSelectionMode(QAbstractItemView::MultiSelection);
filterList->addItems(kColors); // dynamic/fixed, no translation needed filterList->addItems(kEssences); // dynamic/fixed, no translation needed
lay->addWidget(filterList); lay->addWidget(filterList);
showColorRows = new QCheckBox(this); showColorRows = new QCheckBox(this);
@ -56,9 +63,9 @@ void ManaDistributionConfigDialog::retranslateUi()
displayType->setItemText(0, tr("pie")); displayType->setItemText(0, tr("pie"));
displayType->setItemText(1, tr("bar")); 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 // QDialogButtonBox buttons are automatically translated
} }

View file

@ -48,5 +48,5 @@ void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int p
void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent) void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent)
{ {
productionBar->setValue(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));
} }

View file

@ -14,18 +14,25 @@
#include <QList> #include <QList>
#include <QSharedPointer> #include <QSharedPointer>
#include <QStringList> #include <QStringList>
#include <libcockatrice/card/essence_types.h>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
namespace namespace
{ {
AnalyticsPanelWidgetRegistrar registerManaDistribution{ AnalyticsPanelWidgetRegistrar registerManaDistribution{
"manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"), "manaProdDevotion", ManaDistributionWidget::tr("Essence Production + Costs"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }}; [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }};
} // anonymous namespace } // 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) ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer) : AbstractAnalyticsPanelWidget(parent, analyzer)
@ -49,7 +56,7 @@ ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisti
row = new QHBoxLayout(); row = new QHBoxLayout();
containerLayout->addLayout(row); containerLayout->addLayout(row);
for (const QString &c : kColors) { for (const QString &c : kEssences) {
auto *w = new ManaDistributionSingleDisplayWidget(c, this); auto *w = new ManaDistributionSingleDisplayWidget(c, this);
row->addWidget(w); row->addWidget(w);
rows[c] = w; rows[c] = w;
@ -65,7 +72,7 @@ void ManaDistributionWidget::updateDisplay()
const auto &prodPips = analyzer->getProductionPipCount(); const auto &prodPips = analyzer->getProductionPipCount();
const auto &prodCards = analyzer->getProductionCardCount(); 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; QMap<QString, int> devMap, prodMap;
for (const QString &c : filtered) { for (const QString &c : filtered) {
@ -89,7 +96,7 @@ void ManaDistributionWidget::updateDisplay()
productionBarTop->setColors(prodMap); productionBarTop->setColors(prodMap);
} }
for (const QString &c : kColors) { for (const QString &c : kEssences) {
auto *w = rows.value(c); auto *w = rows.value(c);
if (!w) { if (!w) {
@ -126,7 +133,7 @@ void ManaDistributionWidget::updateDisplay()
QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent) QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent)
{ {
auto *dlg = new ManaDistributionConfigDialog(analyzer, parent); auto *dlg = new ManaDistributionConfigDialog(analyzer, parent);
dlg->setWindowTitle(tr("Mana Distribution Settings")); dlg->setWindowTitle(tr("Essence Distribution Settings"));
dlg->setFromConfig(config); dlg->setFromConfig(config);
connect(dlg, &QDialog::accepted, [this, dlg]() { connect(dlg, &QDialog::accepted, [this, dlg]() {

View file

@ -2,6 +2,7 @@
#include "deck_list_model.h" #include "deck_list_model.h"
#include "libcockatrice/card/card_info.h" #include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/deck_list/tree/deck_list_card_node.h" #include "libcockatrice/deck_list/tree/deck_list_card_node.h"
#include "libcockatrice/deck_list/tree/inner_deck_list_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> DeckListStatisticsAnalyzer::determineManaProduction(const QString &rulesText)
{ {
QHash<QString, int> manaCounts = {{"W", 0}, {"U", 0}, {"B", 0}, {"R", 0}, {"G", 0}, {"C", 0}}; QHash<QString, int> result;
for (const QChar code : Essence::symbols()) {
QString text = rulesText.toLower(); // Normalize case for matching result.insert(QString(code), 0);
// 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"]++;
} }
// Optimized regex for specific mana symbols // Explicit Essence grants only. Inferring additional resources from MTG
static const QRegularExpression specificColorRegex(R"(\{T\}:\s*Add\s*\{([WUBRG])\})"); // tap/mana rules would invent Continuum Universes gameplay semantics.
QRegularExpressionMatch match = specificColorRegex.match(rulesText); // E.g. "Add {M}" / "add {N}" both work.
if (match.hasMatch()) { static const QRegularExpression grantedEssence(
manaCounts[match.captured(1)]++; 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}}; std::unordered_map<char, int> result;
for (const QChar code : Essence::symbols()) {
int len = manaString.length(); result.emplace(code.toLatin1(), 0);
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()]++;
}
} }
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) // Hypergeometric probability: P(X=k)

View file

@ -152,7 +152,7 @@ private:
std::unordered_map<int, int> manaCurveMap; std::unordered_map<int, int> manaCurveMap;
std::unordered_map<char, int> manaDevotionMap; 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> devotionCardCount; // how many cards provide devotion
QHash<QString, int> productionPipCount; // mana produced by cards QHash<QString, int> productionPipCount; // mana produced by cards

View file

@ -1,6 +1,6 @@
#include "color_bar.h" #include "color_bar.h"
#include "libcockatrice/utility/color.h" #include "../../../../../essence_chart_colors.h"
#include <QBrush> #include <QBrush>
#include <QLinearGradient> #include <QLinearGradient>
@ -19,14 +19,14 @@
#include <utility> #include <utility>
ColorBar::ColorBar(const QMap<QString, int> &_colors, QWidget *parent) ColorBar::ColorBar(const QMap<QString, int> &_colors, QWidget *parent)
: QWidget(parent), colors(GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors)) : QWidget(parent), colors(EssenceCharts::ordered(_colors))
{ {
setMouseTracking(true); setMouseTracking(true);
} }
void ColorBar::setColors(const QMap<QString, int> &_colors) void ColorBar::setColors(const QMap<QString, int> &_colors)
{ {
colors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors); colors = EssenceCharts::ordered(_colors);
update(); update();
} }
@ -150,19 +150,5 @@ QString ColorBar::tooltipForPosition(int x) const
QColor ColorBar::colorFromName(const QString &name) const QColor ColorBar::colorFromName(const QString &name) const
{ {
static QMap<QString, QColor> map = { return EssenceCharts::colorFor(name);
{"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;
}

View file

@ -1,6 +1,6 @@
#include "color_pie.h" #include "color_pie.h"
#include "libcockatrice/utility/color.h" #include "../../../../../essence_chart_colors.h"
#include <QBrush> #include <QBrush>
#include <QFontMetrics> #include <QFontMetrics>
@ -20,14 +20,14 @@
#include <utility> #include <utility>
ColorPie::ColorPie(const QMap<QString, int> &_colors, QWidget *parent) ColorPie::ColorPie(const QMap<QString, int> &_colors, QWidget *parent)
: QWidget(parent), colors(GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors)) : QWidget(parent), colors(EssenceCharts::ordered(_colors))
{ {
setMouseTracking(true); setMouseTracking(true);
} }
void ColorPie::setColors(const QMap<QString, int> &_colors) void ColorPie::setColors(const QMap<QString, int> &_colors)
{ {
colors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors); colors = EssenceCharts::ordered(_colors);
update(); update();
} }
@ -184,19 +184,5 @@ QString ColorPie::tooltipForPoint(const QPoint &pt) const
QColor ColorPie::colorFromName(const QString &name) const QColor ColorPie::colorFromName(const QString &name) const
{ {
static QMap<QString, QColor> map = { return EssenceCharts::colorFor(name);
{"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;
}

View file

@ -1,48 +1,44 @@
#include "visual_database_display_color_filter_widget.h" #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 "filter_tree_model.h"
#include "libcockatrice/filters/filter_card.h" #include "libcockatrice/filters/filter_card.h"
#include <QAbstractItemModel>
#include <QComboBox> #include <QComboBox>
#include <QDebug>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPersistentModelIndex>
#include <QSharedPointer> #include <QSharedPointer>
#include <QSizePolicy>
#include <QString> #include <QString>
#include <QVariant> #include <QVariant>
#include <QtGlobal> #include <libcockatrice/card/essence_types.h>
#include <libcockatrice/filters/filter_tree.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, VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(QWidget *parent,
FilterTreeModel *_filterModel) FilterTreeModel *_filterModel)
: QWidget(parent), filterModel(_filterModel), layout(new QHBoxLayout(this)) : QWidget(parent), filterModel(_filterModel), layout(new QHBoxLayout(this))
{ {
setLayout(layout); setLayout(layout);
layout->setSpacing(5); layout->setSpacing(3);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
QString fullColorIdentity = "WUBRG"; // Share the same compact Essence selection UI as Visual Deck Storage.
for (const QChar &color : fullColorIdentity) { // A 15-icon horizontal row cannot fit the database filter toolbar.
auto *manaSymbol = new ManaSymbolWidget(this, color, false, true); essencePicker = new EssenceFilterPicker(this);
manaSymbol->setFixedWidth(25); 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(essencePicker, &EssenceFilterPicker::selectionCleared, this, [this]() {
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this, filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
&VisualDatabaseDisplayColorFilterWidget::handleColorToggled); });
}
modeComboBox = new QComboBox(this); modeComboBox = new QComboBox(this);
layout->addWidget(modeComboBox); essencePicker->addPopupControl(modeComboBox);
connect(modeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, connect(modeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&VisualDatabaseDisplayColorFilterWidget::updateFilterMode); &VisualDatabaseDisplayColorFilterWidget::updateFilterMode);
@ -50,108 +46,83 @@ VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(Q
connect(filterModel, &FilterTreeModel::layoutChanged, this, connect(filterModel, &FilterTreeModel::layoutChanged, this,
[this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); }); [this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); });
// Call retranslateUi to set the initial text
retranslateUi(); retranslateUi();
syncWithFilterModel();
} }
void VisualDatabaseDisplayColorFilterWidget::retranslateUi() void VisualDatabaseDisplayColorFilterWidget::retranslateUi()
{ {
essencePicker->retranslateUi();
modeComboBox->blockSignals(true); modeComboBox->blockSignals(true);
modeComboBox->clear(); modeComboBox->clear();
modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch)); modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch));
modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes));
modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); 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)); const int index = modeComboBox->findData(QVariant::fromValue(currentMode));
if (index >= 0) { if (index >= 0) {
modeComboBox->setCurrentIndex(index); modeComboBox->setCurrentIndex(index);
} }
modeComboBox->blockSignals(false); modeComboBox->blockSignals(false);
} }
void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar essence, bool active)
{ {
if (active) { if (active) {
addFilter(color); addFilter(essence);
} else { } else {
removeFilter(color); removeFilter(essence);
} }
} }
void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar essence)
{ {
QString colorString = color; QString essenceString(essence);
QString typeStr; const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
for (const CardFilter *filter : filters) {
// Remove previous filters if (filter->term() == essenceString) {
filterModel->removeFilter(filter);
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);
} }
} }
for (const CardFilter *filter : matchingFilters) {
filterModel->removeFilter(filter);
}
// Add actual filter
switch (currentMode) { switch (currentMode) {
case FilterMode::ExactMatch: 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; break;
case FilterMode::Includes: case FilterMode::Includes:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
break;
case FilterMode::IncludeExclude: 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; break;
} }
} }
void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar essence)
{ {
QString colorString = color; QString essenceString(essence);
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
// Remove inclusion filters for (const CardFilter *filter : filters) {
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); if (filter->term() == essenceString) {
QList<const CardFilter *> matchingFilters; filterModel->removeFilter(filter);
for (const CardFilter *filter : allColorFilters) {
if (filter->term() == color) {
matchingFilters.append(filter);
} }
} }
for (const CardFilter *filter : matchingFilters) { // The empty selection means "Any Essence", not "exclude everything".
filterModel->removeFilter(filter); if (essencePicker->activeEssences().isEmpty()) {
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
return;
} }
// Add exclusion filters if the mode demands it
switch (currentMode) { switch (currentMode) {
case FilterMode::ExactMatch: case FilterMode::ExactMatch:
filterModel->addFilter(
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
break;
case FilterMode::IncludeExclude: case FilterMode::IncludeExclude:
filterModel->addFilter( filterModel->addFilter(
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); new CardFilter(essenceString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
break; break;
case FilterMode::Includes: case FilterMode::Includes:
// No exclusion in Includes mode
break; break;
} }
} }
@ -170,10 +141,12 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor); filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
const QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>(); const QSet<QChar> selected = essencePicker->activeEssences();
// No selections should never generate 15 mutually excluding filters.
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { if (!selected.isEmpty()) {
handleColorToggled(manaSymbolWidget->getSymbolChar(), manaSymbolWidget->isColorActive()); for (const QChar essence : Essence::symbols()) {
handleColorToggled(essence, selected.contains(essence));
}
} }
filterModel->blockSignals(false); filterModel->blockSignals(false);
@ -184,79 +157,44 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
emit filterModeChanged(currentMode); emit filterModeChanged(currentMode);
} }
void VisualDatabaseDisplayColorFilterWidget::setManaSymbolActive(QChar color, bool active) void VisualDatabaseDisplayColorFilterWidget::setEssenceActive(QChar essence, bool active)
{ {
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>(); essencePicker->setEssenceActive(essence, active);
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
if (manaSymbolWidget->getSymbolChar() == color) {
manaSymbolWidget->setColorActive(active);
}
}
} }
QList<QChar> VisualDatabaseDisplayColorFilterWidget::getActiveColors() QList<QChar> VisualDatabaseDisplayColorFilterWidget::getActiveColors()
{ {
QList<QChar> activeColors; QList<QChar> result;
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>(); const QSet<QChar> selected = essencePicker->activeEssences();
for (const QChar essence : Essence::symbols()) {
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { if (selected.contains(essence)) {
if (manaSymbolWidget->isColorActive()) { result.append(essence);
activeColors.append(manaSymbolWidget->getSymbolChar());
} }
} }
return result;
return activeColors;
} }
void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() 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 (const QChar essence : Essence::symbols()) {
bool selected = false;
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { for (const CardFilter *filter : filters) {
bool found = false; if (filter->term() != QString(essence)) {
for (const CardFilter *filter : allColorFilters) { continue;
if (manaSymbolWidget->getSymbolChar() == filter->term()) { }
switch (currentMode) { if (currentMode == FilterMode::ExactMatch) {
case FilterMode::ExactMatch: selected = filter->type() == CardFilter::Type::TypeAnd;
switch (filter->type()) { } else {
case CardFilter::Type::TypeAnd: selected = filter->type() == CardFilter::Type::TypeOr;
setManaSymbolActive(filter->term().at(0), true); }
break; if (selected) {
default: break;
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;
} }
} }
// Signals are blocked inside the picker; loading saved filters must
if (!found) { // not mutate the FilterTreeModel again.
setManaSymbolActive(manaSymbolWidget->getSymbolChar(), false); setEssenceActive(essence, selected);
}
} }
} }

View file

@ -18,6 +18,7 @@
class FilterTreeModel; class FilterTreeModel;
class QComboBox; class QComboBox;
class QHBoxLayout; class QHBoxLayout;
class EssenceFilterPicker;
class VisualDatabaseDisplayColorFilterCircleWidget : public QWidget class VisualDatabaseDisplayColorFilterCircleWidget : public QWidget
{ {
@ -63,7 +64,7 @@ private slots:
void addFilter(QChar color); void addFilter(QChar color);
void removeFilter(QChar color); void removeFilter(QChar color);
void updateFilterMode(); void updateFilterMode();
void setManaSymbolActive(QChar color, bool active); void setEssenceActive(QChar essence, bool active);
QList<QChar> getActiveColors(); QList<QChar> getActiveColors();
void syncWithFilterModel(); void syncWithFilterModel();
@ -71,6 +72,7 @@ private:
FilterTreeModel *filterModel; FilterTreeModel *filterModel;
QHBoxLayout *layout; QHBoxLayout *layout;
QComboBox *modeComboBox; QComboBox *modeComboBox;
EssenceFilterPicker *essencePicker = nullptr;
FilterMode currentMode = FilterMode::Includes; // Default mode FilterMode currentMode = FilterMode::Includes; // Default mode
QtUtils::Debouncer syncDebouncer{100}; QtUtils::Debouncer syncDebouncer{100};
}; };

View file

@ -1,12 +1,12 @@
#include "deck_preview_color_identity_filter_widget.h" #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 "../visual_deck_storage_sort_filter_proxy_model.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPushButton> #include <QPushButton>
#include <QSizePolicy>
#include <QSet> #include <QSet>
#include <QSharedPointer>
#include <QString> #include <QString>
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent) DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent)
@ -16,34 +16,30 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidg
layout->setSpacing(5); layout->setSpacing(5);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
QString fullColorIdentity = "WUBRG"; // Fifteen Essence codes don't fit as inline buttons beside sorting and
for (const QChar &color : fullColorIdentity) { // search. The popup keeps all codes selectable without expanding the row.
auto *manaSymbol = new ManaSymbolWidget(this, color, false, true); essencePicker = new EssenceFilterPicker(this);
manaSymbol->setFixedWidth(25); layout->addWidget(essencePicker);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
setMinimumWidth(essencePicker->width());
layout->addWidget(manaSymbol); connect(essencePicker, &EssenceFilterPicker::essenceToggled, this,
[this](QChar /*essence*/, bool /*active*/) { emit activeColorsChanged(); });
// Initialize the activeColors map connect(essencePicker, &EssenceFilterPicker::selectionCleared, this,
activeColors[color] = false; [this]() { emit activeColorsChanged(); });
// Connect the color toggled signal
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this,
&DeckPreviewColorIdentityFilterWidget::handleColorToggled);
}
toggleButton = new QPushButton(this); toggleButton = new QPushButton(this);
layout->addWidget(toggleButton); essencePicker->addPopupControl(toggleButton);
// Connect the button's clicked signal
connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode);
// Call retranslateUi to set the initial text
retranslateUi(); retranslateUi();
} }
void DeckPreviewColorIdentityFilterWidget::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) { switch (filterMode) {
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
toggleButton->setText(tr("Mode: Exact Match")); toggleButton->setText(tr("Mode: Exact Match"));
@ -55,32 +51,21 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi()
toggleButton->setText(tr("Mode: Excludes")); toggleButton->setText(tr("Mode: Excludes"));
break; 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> DeckPreviewColorIdentityFilterWidget::getActiveColors() const
{ {
QSet<QChar> activeColorSet; return essencePicker->activeEssences();
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
if (it.value()) {
activeColorSet.insert(it.key());
}
}
return activeColorSet;
} }
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active) void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar /*essence*/, bool /*active*/)
{ {
activeColors[color] = active;
emit activeColorsChanged(); emit activeColorsChanged();
} }
void DeckPreviewColorIdentityFilterWidget::updateFilterMode() void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
{ {
// Cycle through the modes
switch (filterMode) { switch (filterMode) {
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes; filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes;
@ -93,6 +78,6 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
break; break;
} }
retranslateUi(); // Update the button text retranslateUi();
emit filterModeChanged(filterMode); emit filterModeChanged(filterMode);
} }

View file

@ -9,13 +9,13 @@
#include "../visual_deck_storage_sort_filter_proxy_model.h" #include "../visual_deck_storage_sort_filter_proxy_model.h"
#include <QChar> #include <QChar>
#include <QMap>
#include <QSet> #include <QSet>
#include <QWidget> #include <QWidget>
#include <qtmetamacros.h> #include <qtmetamacros.h>
class QHBoxLayout; class QHBoxLayout;
class QPushButton; class QPushButton;
class EssenceFilterPicker;
class DeckPreviewColorIdentityFilterWidget : public QWidget class DeckPreviewColorIdentityFilterWidget : public QWidget
{ {
@ -57,7 +57,7 @@ private slots:
private: private:
QHBoxLayout *layout; QHBoxLayout *layout;
QPushButton *toggleButton; QPushButton *toggleButton;
QMap<QChar, bool> activeColors; EssenceFilterPicker *essencePicker = nullptr;
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes; VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
}; };

View file

@ -10,16 +10,18 @@ Cockatrice's three visual table rows are reused as **Front**, **Support**, and *
## Optional color customization ## 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 ```ini
[Zones] [Zones]
Front = #A08E343D Front = #dc143c
Support = #A0286981 Support = #c3b091
Command = #A05E428C 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 ## Font

View 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.

View 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.

View file

@ -1,5 +1,6 @@
set(HEADERS set(HEADERS
libcockatrice/card/card_info.h libcockatrice/card/card_info.h
libcockatrice/card/essence_types.h
libcockatrice/card/card_info_comparator.h libcockatrice/card/card_info_comparator.h
libcockatrice/card/card_localization.h libcockatrice/card/card_localization.h
libcockatrice/card/lazy_properties_hash.h libcockatrice/card/lazy_properties_hash.h

View file

@ -1,6 +1,7 @@
#include "card_info.h" #include "card_info.h"
#include "game_specific_terms.h" #include "game_specific_terms.h"
#include "essence_types.h"
#include "libcockatrice/card/lazy_properties_hash.h" #include "libcockatrice/card/lazy_properties_hash.h"
#include "printing/printing_info.h" #include "printing/printing_info.h"
#include "relation/card_relation.h" #include "relation/card_relation.h"
@ -304,17 +305,34 @@ void CardInfo::setCardType(const QString &value)
{ {
setProperty(Mtg::CardType, 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 QString CardInfo::getCmc() const
{ {
return getProperty(Mtg::ConvertedManaCost); return getEssenceValue();
} }
QString CardInfo::getColors() const QString CardInfo::getColors() const
{ {
return getProperty(Mtg::Colors); return getEssenceTypes();
} }
void CardInfo::setColors(const QString &value) void CardInfo::setColors(const QString &value)
{ {
setProperty(Mtg::Colors, value); setProperty(EssenceProperties::Types, Essence::normalizeIdentity(value));
} }
QString CardInfo::getLoyalty() const QString CardInfo::getLoyalty() const
{ {
@ -326,11 +344,53 @@ QString CardInfo::getMainCardType() const
} }
QString CardInfo::getManaCost() 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 QString CardInfo::getPowTough() const
{ {
return getProperty(Mtg::PowTough); // Compatibility accessor. Display terminology is always ATK/HP.
return getAtkHp();
} }
void CardInfo::setPowTough(const QString &value) void CardInfo::setPowTough(const QString &value)
{ {

View file

@ -466,12 +466,24 @@ public:
/** @name Legacy/Convenience Property Accessors */ //@{ /** @name Legacy/Convenience Property Accessors */ //@{
[[nodiscard]] QString getCardType() const; [[nodiscard]] QString getCardType() const;
void setCardType(const QString &value); 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 getCmc() const;
[[nodiscard]] QString getColors() const; [[nodiscard]] QString getColors() const;
void setColors(const QString &value); void setColors(const QString &value);
[[nodiscard]] QString getLoyalty() const; [[nodiscard]] QString getLoyalty() const;
[[nodiscard]] QString getMainCardType() const; [[nodiscard]] QString getMainCardType() const;
[[nodiscard]] QString getManaCost() 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; [[nodiscard]] QString getPowTough() const;
void setPowTough(const QString &value); void setPowTough(const QString &value);
//@} //@}

View file

@ -319,7 +319,11 @@ void CardDatabase::swapInDatabaseData(CardDatabaseData data)
sets = std::move(data.sets); sets = std::move(data.sets);
formats = std::move(data.formats); 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 // inform listeners that the whole database was replaced; they should
// rebuild from the live containers in a single batch instead of reacting // rebuild from the live containers in a single batch instead of reacting

View 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

View file

@ -16,6 +16,26 @@
* Note: Mtg = "Maybe that game" * 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 namespace Mtg
{ {
QString const CardType("type"); QString const CardType("type");
@ -31,26 +51,38 @@ QString const ColorIdentity("coloridentity");
inline static const QString getNicePropertyName(QString key) 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) { if (key == CardType) {
return QCoreApplication::translate("Mtg", "Card Type"); 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) { if (key == Loyalty) {
return QCoreApplication::translate("Mtg", "Loyalty"); return QCoreApplication::translate("Mtg", "Loyalty");
} }
if (key == MainCardType) { if (key == MainCardType) {
return QCoreApplication::translate("Mtg", "Main Card Type"); return QCoreApplication::translate("Mtg", "Main Card Type");
} }
if (key == ManaCost) {
return QCoreApplication::translate("Mtg", "Mana Cost");
}
if (key == PowTough) { if (key == PowTough) {
return QCoreApplication::translate("Mtg", "P/T"); return QCoreApplication::translate("Continuum", "ATK/HP");
} }
if (key == Side) { if (key == Side) {
return QCoreApplication::translate("Mtg", "Side"); return QCoreApplication::translate("Mtg", "Side");
@ -58,9 +90,6 @@ inline static const QString getNicePropertyName(QString key)
if (key == Layout) { if (key == Layout) {
return QCoreApplication::translate("Mtg", "Layout"); return QCoreApplication::translate("Mtg", "Layout");
} }
if (key == ColorIdentity) {
return QCoreApplication::translate("Mtg", "Color Identity");
}
return key; return key;
} }
} // namespace Mtg } // namespace Mtg

View file

@ -68,15 +68,15 @@ const QString CardFilter::attrName(Attr a)
case AttrType: case AttrType:
return tr("Type"); return tr("Type");
case AttrColor: case AttrColor:
return tr("Color"); return tr("Essence");
case AttrText: case AttrText:
return tr("Text"); return tr("Text");
case AttrSet: case AttrSet:
return tr("Set"); return tr("Set");
case AttrManaCost: case AttrManaCost:
return tr("Mana Cost"); return tr("Essence Cost");
case AttrCmc: case AttrCmc:
return tr("Mana Value"); return tr("Essence Value");
case AttrRarity: case AttrRarity:
return tr("Rarity"); return tr("Rarity");
case AttrPow: case AttrPow:

View file

@ -2,6 +2,7 @@
#include "filter_card.h" #include "filter_card.h"
#include "libcockatrice/card/card_localization.h" #include "libcockatrice/card/card_localization.h"
#include <libcockatrice/card/essence_types.h>
#include "libcockatrice/card/printing/printing_info.h" #include "libcockatrice/card/printing/printing_info.h"
#include "libcockatrice/card/set/card_set.h" #include "libcockatrice/card/set/card_set.h"
@ -177,23 +178,16 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st
switch (attr()) { switch (attr()) {
case CardFilter::AttrColor: { case CardFilter::AttrColor: {
QString converted = term.trimmed(); // The old MTG color-name conversions are invalid for Essence.
// N is explicitly colorless; an unspecified identity is not N.
converted.replace("green", "g", Qt::CaseInsensitive); QString converted = term.trimmed().toUpper();
converted.replace("grn", "g", Qt::CaseInsensitive); if (converted == QStringLiteral("COLORLESS") || converted == QStringLiteral("COLOURLESS")) {
converted.replace("blue", "u", Qt::CaseInsensitive); converted = QStringLiteral("N");
converted.replace("blu", "u", Qt::CaseInsensitive); }
converted.replace("black", "b", Qt::CaseInsensitive); converted.remove(QLatin1Char(' '));
converted.replace("blk", "b", Qt::CaseInsensitive); converted.remove(QLatin1Char(','));
converted.replace("red", "r", Qt::CaseInsensitive); converted.remove(QLatin1Char('{'));
converted.replace("white", "w", Qt::CaseInsensitive); converted.remove(QLatin1Char('}'));
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);
convertedColorTerm = converted; convertedColorTerm = converted;
break; break;
} }
@ -240,8 +234,7 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st
break; break;
} }
case CardFilter::AttrManaCost: case CardFilter::AttrManaCost:
sortedManaCostTerm = term.toUpper(); sortedManaCostTerm = Essence::parseCost(term.toUpper()).join(QString());
// Sort the mana cost so it will be easy to find
std::sort(sortedManaCostTerm.begin(), sortedManaCostTerm.end()); std::sort(sortedManaCostTerm.begin(), sortedManaCostTerm.end());
break; break;
case CardFilter::AttrLoyalty: case CardFilter::AttrLoyalty:
@ -360,25 +353,16 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const
bool FilterItem::acceptColor(const CardInfoPtr &info) const bool FilterItem::acceptColor(const CardInfoPtr &info) const
{ {
const QString colors = info->getColors(); if (convertedColorTerm.isEmpty()) {
return false;
// Colorless card filter
if (convertedColorTerm.toLower() == "c" && colors.length() < 1) {
return true;
} }
const QString essence = info->getEssenceTypes();
/* for (const QChar code : convertedColorTerm) {
* This is a tricky part, if the filter has multiple colors in it, like UGW, if (!Essence::isSymbol(code) || !essence.contains(code)) {
* then we should match all of them to the card's colors return false;
*/
int match_count = 0;
for (const QChar &color : convertedColorTerm) {
if (colors.contains(color, Qt::CaseInsensitive)) {
match_count++;
} }
} }
return true;
return match_count == convertedColorTerm.length();
} }
bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const 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 bool FilterItem::acceptManaCost(const CardInfoPtr &info) const
{ {
// Try to seperate the mana cost in case it's a split card if (sortedManaCostTerm.isEmpty()) {
// if it's not a split card the loop will run only once return false;
for (const QString &manaCostPart : info->getManaCost().split("//")) { }
QString fullManaCost = manaCostPart; for (const QString &costPart : info->getManaCost().split(QStringLiteral("//"))) {
std::sort(fullManaCost.begin(), fullManaCost.end()); QString sortedCost = Essence::parseCost(costPart).join(QString());
std::sort(sortedCost.begin(), sortedCost.end());
// If the partial is found in the full, return true if (sortedCost.contains(sortedManaCostTerm)) {
if (fullManaCost.contains(sortedManaCostTerm)) {
return true; return true;
} }
} }

View file

@ -70,11 +70,18 @@ bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelInd
if (isRightType && (!isLeftType || rightString.size() == cardName.size())) { if (isRightType && (!isLeftType || rightString.size() == cardName.size())) {
return false; return false;
} }
} else if (right.column() == CardDatabaseModel::PTColumn && left.column() == CardDatabaseModel::PTColumn) { } else if (left.column() == right.column() &&
// The comparator runs O(n log n) times while the view is sorted, so (left.column() == CardDatabaseModel::AttackColumn ||
// splitting every power/toughness string into a QStringList allocates left.column() == CardDatabaseModel::ArmourColumn ||
// a lot for data that has exactly one separator. Work on views and left.column() == CardDatabaseModel::HealthColumn)) {
// only split the two halves out when both strings are well formed. // 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 leftPT = leftString;
const QStringView rightPT = rightString; const QStringView rightPT = rightString;
const qsizetype leftSlash = leftPT.indexOf(u'/'); 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; const bool rightIsWellFormed = rightSlash >= 0 && rightPT.indexOf(u'/', rightSlash + 1) < 0;
if (leftIsWellFormed && rightIsWellFormed) { if (leftIsWellFormed && rightIsWellFormed) {
const int powerComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash)); const int attackComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash));
if (powerComparison != 0) { if (attackComparison != 0) {
return powerComparison < 0; return attackComparison < 0;
} }
// power equal, check toughness // Attack equal, compare Health
return lessThanNumerically(leftPT.mid(leftSlash + 1), rightPT.mid(rightSlash + 1)) < 0; return lessThanNumerically(leftPT.mid(leftSlash + 1), rightPT.mid(rightSlash + 1)) < 0;
} }
} }

View file

@ -11,7 +11,6 @@
class QObject; class QObject;
#define CARDDBMODEL_COLUMNS 6
CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent) CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent)
: QAbstractListModel(parent), db(_db), showOnlyCardsFromEnabledSets(_showOnlyCardsFromEnabledSets) : 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 // 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. // 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 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)) { (role != Qt::DisplayRole && role != SortRole)) {
return QVariant(); return QVariant();
} }
@ -59,10 +58,18 @@ QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const
: card->getManaCost(); : card->getManaCost();
case CardTypeColumn: case CardTypeColumn:
return card->getCardType(); return card->getCardType();
case PTColumn: case AtkHpColumn:
return card->getPowTough(); return card->getAtkHp();
case ColorColumn: case ColorColumn:
return card->getColors(); return card->getColors();
case IdentityColumn:
return card->getIdentity();
case AttackColumn:
return card->getAttack();
case ArmourColumn:
return card->getArmour();
case HealthColumn:
return card->getHealth();
default: default:
return QVariant(); return QVariant();
} }
@ -82,13 +89,21 @@ QVariant CardDatabaseModel::headerData(int section, Qt::Orientation orientation,
case SetListColumn: case SetListColumn:
return QString(tr("Sets")); return QString(tr("Sets"));
case ManaCostColumn: case ManaCostColumn:
return QString(tr("Mana cost")); return QString(tr("Essence Cost"));
case CardTypeColumn: case CardTypeColumn:
return QString(tr("Card type")); return QString(tr("Card type"));
case PTColumn: case AtkHpColumn:
return QString(tr("P/T")); return QString(tr("ATK/HP"));
case ColorColumn: 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: default:
return QVariant(); return QVariant();
} }
@ -101,7 +116,7 @@ void CardDatabaseModel::cardInfoChanged(const CardInfoPtr &card)
return; 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 bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card) const

View file

@ -28,9 +28,15 @@ public:
NameColumn, NameColumn,
SetListColumn, SetListColumn,
ManaCostColumn, ManaCostColumn,
PTColumn, AtkHpColumn, // Formerly P/T; keep column index for saved table layouts.
CardTypeColumn, CardTypeColumn,
ColorColumn ColorColumn,
IdentityColumn,
AttackColumn,
ArmourColumn,
HealthColumn,
ColumnCount,
PTColumn = AtkHpColumn // Compatibility alias for inherited callers.
}; };
enum Role enum Role
{ {

View file

@ -172,6 +172,13 @@ endfunction()
# --- Unit tests ------------------------------------------------------------------ # --- Unit tests ------------------------------------------------------------------
cockatrice_add_test(NAME expression_test SOURCES expression_test.cpp LIBS libcockatrice_utility) 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 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 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) cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility)

View file

@ -26,6 +26,7 @@
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_data.h> #include <libcockatrice/card/database/card_database_data.h>
#include <libcockatrice/card/database/parser/cockatrice_xml_4.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/lazy_properties_hash.h>
#include <libcockatrice/card/printing/printing_info.h> #include <libcockatrice/card/printing/printing_info.h>
#include <libcockatrice/card/relation/card_relation.h> #include <libcockatrice/card/relation/card_relation.h>
@ -35,6 +36,7 @@
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h> #include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/utility/card_ref.h> #include <libcockatrice/utility/card_ref.h>
#include <string> #include <string>
#include <utility>
namespace namespace
{ {
@ -65,6 +67,65 @@ TEST(CardDatabaseTest, LoadXml)
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; 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) TEST(CardDatabaseTest, LazyPropertiesHashStreamsBlobAccessors)
{ {
QHash<QString, QString> props; QHash<QString, QString> props;

View 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"));
}

View file

@ -103,6 +103,54 @@ TEST(ModelContract, CardDatabaseModelAndDisplayProxyKeepContract)
EXPECT_LT(display.rowCount(), source.rowCount()); 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) TEST(ModelContract, SetsModelKeepsContract)
{ {
CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(),