diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc
index 4ec3d2385..e421553ab 100644
--- a/cockatrice/cockatrice.qrc
+++ b/cockatrice/cockatrice.qrc
@@ -5,6 +5,9 @@
resources/cockatrice-logo-white.svg
resources/hand.svg
resources/hr.jpg
+ resources/piles/limbo.svg
+ resources/piles/refuge.svg
+ resources/piles/oblivion.svg
resources/icons/arrow_bottom_green.svg
resources/icons/arrow_down_green.svg
diff --git a/cockatrice/resources/piles/limbo.svg b/cockatrice/resources/piles/limbo.svg
new file mode 100644
index 000000000..288b195cc
--- /dev/null
+++ b/cockatrice/resources/piles/limbo.svg
@@ -0,0 +1,7 @@
+
diff --git a/cockatrice/resources/piles/oblivion.svg b/cockatrice/resources/piles/oblivion.svg
new file mode 100644
index 000000000..96e832b8e
--- /dev/null
+++ b/cockatrice/resources/piles/oblivion.svg
@@ -0,0 +1,7 @@
+
diff --git a/cockatrice/resources/piles/refuge.svg b/cockatrice/resources/piles/refuge.svg
new file mode 100644
index 000000000..8c42ef953
--- /dev/null
+++ b/cockatrice/resources/piles/refuge.svg
@@ -0,0 +1,6 @@
+
diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp
index 9f00c0cba..f00a53520 100644
--- a/cockatrice/src/game_graphics/player/menu/card_menu.cpp
+++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp
@@ -178,7 +178,10 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcuts
} else if (card->getZone()->getName() == ZoneNames::STACK) {
createStackMenu(writeableCard);
} else if (card->getZone()->getName() == ZoneNames::EXILE ||
- card->getZone()->getName() == ZoneNames::GRAVE) {
+ card->getZone()->getName() == ZoneNames::GRAVE ||
+ card->getZone()->getName() == ZoneNames::LIMBO ||
+ card->getZone()->getName() == ZoneNames::REFUGE ||
+ card->getZone()->getName() == ZoneNames::OBLIVION) {
createGraveyardOrExileMenu(writeableCard);
} else {
createHandOrCustomZoneMenu(writeableCard);
diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp
index b81c4619b..81ede71a4 100644
--- a/cockatrice/src/game_graphics/player/menu/move_menu.cpp
+++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp
@@ -25,6 +25,12 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
aMoveToGraveyard->setData(cmMoveToGraveyard);
aMoveToExile = new QAction(this);
aMoveToExile->setData(cmMoveToExile);
+ aMoveToLimbo = new QAction(this);
+ aMoveToLimbo->setData(cmMoveToLimbo);
+ aMoveToRefuge = new QAction(this);
+ aMoveToRefuge->setData(cmMoveToRefuge);
+ aMoveToOblivion = new QAction(this);
+ aMoveToOblivion->setData(cmMoveToOblivion);
auto *actions = player->getLogic()->getPlayerActions();
@@ -42,6 +48,9 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
connect(aMoveToHand, &QAction::triggered, actions, invoke(cmMoveToHand));
connect(aMoveToGraveyard, &QAction::triggered, actions, invoke(cmMoveToGraveyard));
connect(aMoveToExile, &QAction::triggered, actions, invoke(cmMoveToExile));
+ connect(aMoveToLimbo, &QAction::triggered, actions, invoke(cmMoveToLimbo));
+ connect(aMoveToRefuge, &QAction::triggered, actions, invoke(cmMoveToRefuge));
+ connect(aMoveToOblivion, &QAction::triggered, actions, invoke(cmMoveToOblivion));
addAction(aMoveToTopLibrary);
addAction(aMoveToXfromTopOfLibrary);
@@ -54,6 +63,10 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
addAction(aMoveToGraveyard);
addSeparator();
addAction(aMoveToExile);
+ addSeparator();
+ addAction(aMoveToLimbo);
+ addAction(aMoveToRefuge);
+ addAction(aMoveToOblivion);
setShortcutsActive();
@@ -81,4 +94,7 @@ void MoveMenu::retranslateUi()
aMoveToHand->setText(tr("&Hand"));
aMoveToGraveyard->setText(tr("&Graveyard"));
aMoveToExile->setText(tr("&Exile"));
+ aMoveToLimbo->setText(tr("Limbo"));
+ aMoveToRefuge->setText(tr("Refuge"));
+ aMoveToOblivion->setText(tr("Oblivion"));
}
diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.h b/cockatrice/src/game_graphics/player/menu/move_menu.h
index f1f239abd..b2aee973e 100644
--- a/cockatrice/src/game_graphics/player/menu/move_menu.h
+++ b/cockatrice/src/game_graphics/player/menu/move_menu.h
@@ -29,6 +29,9 @@ public:
QAction *aMoveToTable = nullptr;
QAction *aMoveToGraveyard = nullptr;
QAction *aMoveToExile = nullptr;
+ QAction *aMoveToLimbo = nullptr;
+ QAction *aMoveToRefuge = nullptr;
+ QAction *aMoveToOblivion = nullptr;
};
#endif // COCKATRICE_MOVE_MENU_H
diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp
index 582586ac2..0ecd6f21b 100644
--- a/cockatrice/src/game_graphics/player/menu/player_menu.cpp
+++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp
@@ -20,12 +20,14 @@
#include "tally_menu.h"
#include "utility_menu.h"
+#include
#include
#include
#include
#include
#include
#include
+#include
PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player)
{
@@ -45,6 +47,19 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
graveMenu = addManagedMenu(player, playerMenu);
rfgMenu = addManagedMenu(player, playerMenu);
+ // Context menus with double-click view action, following Exile's pattern.
+ const auto addPublicPileMenu = [this, _player](const QString &title, const QString &zone) {
+ auto *menu = playerMenu->addMenu(title);
+ auto *view = menu->addAction(tr("View %1").arg(title));
+ connect(view, &QAction::triggered, this, [_player, zone]() {
+ _player->getLogic()->onRequestZoneViewToggle(zone, -1, false);
+ });
+ return menu;
+ };
+ limboMenu = addPublicPileMenu(tr("Limbo"), QString::fromLatin1(ZoneNames::LIMBO));
+ refugeMenu = addPublicPileMenu(tr("Refuge"), QString::fromLatin1(ZoneNames::REFUGE));
+ oblivionMenu = addPublicPileMenu(tr("Oblivion"), QString::fromLatin1(ZoneNames::OBLIVION));
+
if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) {
sideboardMenu = addManagedMenu(player, playerMenu);
customZonesMenu = addManagedMenu(player);
@@ -80,6 +95,9 @@ void PlayerMenu::setMenusForGraphicItems()
player->getTableZoneGraphicsItem()->setMenu(playerMenu);
player->getGraveyardZoneGraphicsItem()->setMenu(graveMenu, graveMenu->aViewGraveyard);
player->getRfgZoneGraphicsItem()->setMenu(rfgMenu, rfgMenu->aViewRfg);
+ player->getLimboZoneGraphicsItem()->setMenu(limboMenu, limboMenu->actions().constFirst());
+ player->getRefugeZoneGraphicsItem()->setMenu(refugeMenu, refugeMenu->actions().constFirst());
+ player->getOblivionZoneGraphicsItem()->setMenu(oblivionMenu, oblivionMenu->actions().constFirst());
if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) {
player->getHandZoneGraphicsItem()->setMenu(handMenu);
player->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard);
diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h
index e21a689de..48c12be5c 100644
--- a/cockatrice/src/game_graphics/player/menu/player_menu.h
+++ b/cockatrice/src/game_graphics/player/menu/player_menu.h
@@ -92,6 +92,9 @@ private:
SideboardMenu *sideboardMenu;
GraveyardMenu *graveMenu;
RfgMenu *rfgMenu;
+ QMenu *limboMenu = nullptr;
+ QMenu *refugeMenu = nullptr;
+ QMenu *oblivionMenu = nullptr;
UtilityMenu *utilityMenu;
TallyMenu *tallyMenu;
SayMenu *sayMenu;
diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
index 0805dbb5b..fbc70680d 100644
--- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp
+++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
@@ -49,6 +49,11 @@
#include
#include
+namespace
+{
+constexpr qreal EXTRA_PILE_COLUMN_WIDTH = CardDimensions::HEIGHT_F + 24;
+} // namespace
+
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
{
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
@@ -152,6 +157,16 @@ void PlayerGraphicsItem::initializeZones()
rfgZoneGraphicsItem = new PileZone(player->getRfgZone(), this);
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
+ // Three public piles in a second column, beside the legacy piles.
+ const qreal extraPileX = CardDimensions::HEIGHT_F + counterAreaWidth + 15 + 8;
+ limboZoneGraphicsItem = new PileZone(player->getLimboZone(), this, tr("Limbo"), QStringLiteral("limbo"));
+ limboZoneGraphicsItem->setPos(QPointF(extraPileX, base.y()));
+ refugeZoneGraphicsItem = new PileZone(player->getRefugeZone(), this, tr("Refuge"), QStringLiteral("refuge"));
+ refugeZoneGraphicsItem->setPos(QPointF(extraPileX, base.y() + h + h2 + 10));
+ oblivionZoneGraphicsItem = new PileZone(player->getOblivionZone(), this, tr("Oblivion"),
+ QStringLiteral("oblivion"));
+ oblivionZoneGraphicsItem->setPos(QPointF(extraPileX, base.y() + 2 * h + h2 + 10));
+
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
@@ -169,6 +184,9 @@ void PlayerGraphicsItem::initializeZones()
zoneGraphicsItems.insert(player->getDeckZone()->getName(), deckZoneGraphicsItem);
zoneGraphicsItems.insert(player->getGraveZone()->getName(), graveyardZoneGraphicsItem);
zoneGraphicsItems.insert(player->getRfgZone()->getName(), rfgZoneGraphicsItem);
+ zoneGraphicsItems.insert(player->getLimboZone()->getName(), limboZoneGraphicsItem);
+ zoneGraphicsItems.insert(player->getRefugeZone()->getName(), refugeZoneGraphicsItem);
+ zoneGraphicsItems.insert(player->getOblivionZone()->getName(), oblivionZoneGraphicsItem);
zoneGraphicsItems.insert(player->getSideboardZone()->getName(), sideboardGraphicsItem);
zoneGraphicsItems.insert(player->getTableZone()->getName(), tableZoneGraphicsItem);
zoneGraphicsItems.insert(player->getStackZone()->getName(), stackZoneGraphicsItem);
@@ -203,7 +221,7 @@ QRectF PlayerGraphicsItem::boundingRect() const
qreal PlayerGraphicsItem::getMinimumWidth() const
{
- qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
+ qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth + EXTRA_PILE_COLUMN_WIDTH +
stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
result += handZoneGraphicsItem->boundingRect().width();
@@ -272,7 +290,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
{
// Extend table (and hand, if horizontal) to accommodate the new player width.
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
- stackZoneGraphicsItem->boundingRect().width();
+ EXTRA_PILE_COLUMN_WIDTH - stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
tableWidth -= handZoneGraphicsItem->boundingRect().width();
}
@@ -345,7 +363,7 @@ void PlayerGraphicsItem::rearrangeCounters()
void PlayerGraphicsItem::rearrangeZones()
{
- auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
+ auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15 + EXTRA_PILE_COLUMN_WIDTH, 0);
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
if (mirrored) {
if (player->getHandZone()->contentsKnown()) {
@@ -396,7 +414,7 @@ void PlayerGraphicsItem::rearrangeZones()
void PlayerGraphicsItem::updateBoundingRect()
{
prepareGeometryChange();
- qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
+ qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + EXTRA_PILE_COLUMN_WIDTH + stackZoneGraphicsItem->boundingRect().width();
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h
index e1ca0a036..237fccb55 100644
--- a/cockatrice/src/game_graphics/player/player_graphics_item.h
+++ b/cockatrice/src/game_graphics/player/player_graphics_item.h
@@ -114,6 +114,18 @@ public:
{
return rfgZoneGraphicsItem;
}
+ [[nodiscard]] PileZone *getLimboZoneGraphicsItem() const
+ {
+ return limboZoneGraphicsItem;
+ }
+ [[nodiscard]] PileZone *getRefugeZoneGraphicsItem() const
+ {
+ return refugeZoneGraphicsItem;
+ }
+ [[nodiscard]] PileZone *getOblivionZoneGraphicsItem() const
+ {
+ return oblivionZoneGraphicsItem;
+ }
[[nodiscard]] TableZone *getTableZoneGraphicsItem() const
{
return tableZoneGraphicsItem;
@@ -154,6 +166,9 @@ private:
PileZone *sideboardGraphicsItem;
PileZone *graveyardZoneGraphicsItem;
PileZone *rfgZoneGraphicsItem;
+ PileZone *limboZoneGraphicsItem;
+ PileZone *refugeZoneGraphicsItem;
+ PileZone *oblivionZoneGraphicsItem;
TableZone *tableZoneGraphicsItem;
StackZone *stackZoneGraphicsItem;
HandZone *handZoneGraphicsItem;
diff --git a/cockatrice/src/game_graphics/zones/pile_zone.cpp b/cockatrice/src/game_graphics/zones/pile_zone.cpp
index 08af241b6..eeb6af987 100644
--- a/cockatrice/src/game_graphics/zones/pile_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/pile_zone.cpp
@@ -1,11 +1,16 @@
#include "pile_zone.h"
#include "../../client/settings/cache_settings.h"
+#include "../../interface/theme_manager.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
#include "../card_dimensions.h"
#include
+#include
+#include
+#include
+#include
#include
#include
#include
@@ -28,12 +33,19 @@
#include
#include
-PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
+PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent,
+ const QString &displayName, const QString &iconAsset)
+ : CardZone(_logic, parent), customDisplayName(displayName), customIconAsset(iconAsset)
{
setCacheMode(DeviceCoordinateCache); // Do not move this line to the parent constructor!
setAcceptHoverEvents(true);
setCursor(Qt::OpenHandCursor);
+ if (!customIconAsset.isEmpty()) {
+ connect(themeManager, &ThemeManager::themeChanged, this, &PileZone::reloadCustomIcon);
+ reloadCustomIcon();
+ }
+
setTransform(QTransform()
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(90)
@@ -48,6 +60,18 @@ PileZone::PileZone(PileZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_log
});
}
+void PileZone::reloadCustomIcon()
+{
+ // Try the active theme's piles/ asset first, then the bundled SVG.
+ const QString path = QStringLiteral("piles/") + customIconAsset;
+ customIconPixmap = QPixmap(QStringLiteral("theme:") + themeManager->assetPath(path));
+ if (customIconPixmap.isNull()) {
+ customIconPixmap =
+ QPixmap(QStringLiteral(":/resources/piles/") + customIconAsset + QStringLiteral(".svg"));
+ }
+ update();
+}
+
QRectF PileZone::boundingRect() const
{
return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F);
@@ -70,10 +94,34 @@ void PileZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio
cardItems().at(0)->paintPicture(painter, cardItems().at(0)->getTranslatedSize(painter), 90);
}
+ painter->save();
painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F);
painter->rotate(-90);
painter->translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F);
paintNumberEllipse(cardItems().size(), 28, Qt::white, -1, -1, painter);
+ painter->restore();
+
+ if (!customDisplayName.isEmpty()) {
+ painter->save();
+ // Undo the pile's 90-degree scene rotation to make the badge readable.
+ painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F);
+ painter->rotate(-90);
+ painter->translate(-CardDimensions::HEIGHT_HALF_F, -CardDimensions::WIDTH_HALF_F);
+
+ const qreal labelWidth = CardDimensions::HEIGHT_F;
+ painter->fillRect(QRectF(2, 2, labelWidth - 4, 29), QColor(16, 18, 28, 220));
+ if (!customIconPixmap.isNull()) {
+ painter->drawPixmap(QRect(7, 6, 21, 21), customIconPixmap);
+ }
+ QFont font = painter->font();
+ font.setPixelSize(12);
+ font.setBold(true);
+ painter->setFont(font);
+ painter->setPen(Qt::white);
+ painter->drawText(QRectF(31, 3, labelWidth - 34, 26), Qt::AlignLeft | Qt::AlignVCenter,
+ customDisplayName.toUpper());
+ painter->restore();
+ }
}
void PileZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &)
diff --git a/cockatrice/src/game_graphics/zones/pile_zone.h b/cockatrice/src/game_graphics/zones/pile_zone.h
index fa510ad52..777fcd071 100644
--- a/cockatrice/src/game_graphics/zones/pile_zone.h
+++ b/cockatrice/src/game_graphics/zones/pile_zone.h
@@ -9,6 +9,8 @@
#include "card_zone.h"
+#include
+#include
#include
class PileZoneLogic;
@@ -21,6 +23,11 @@ class QGraphicsItem;
class PileZone : public CardZone
{
Q_OBJECT
+private:
+ QString customDisplayName;
+ QString customIconAsset;
+ QPixmap customIconPixmap;
+ void reloadCustomIcon();
private slots:
void callUpdate()
{
@@ -28,7 +35,8 @@ private slots:
}
public:
- PileZone(PileZoneLogic *_logic, QGraphicsItem *parent);
+ PileZone(PileZoneLogic *_logic, QGraphicsItem *parent,
+ const QString &displayName = {}, const QString &iconAsset = {});
[[nodiscard]] QRectF boundingRect() const override;
[[nodiscard]] QPainterPath shape() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp
index c857a5a67..f643b3fd5 100644
--- a/cockatrice/src/game_graphics/zones/table_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/table_zone.cpp
@@ -206,23 +206,23 @@ void TableZone::paintBattlefieldRows(QPainter *painter)
painter->setFont(font);
for (int visualRow = 0; visualRow < TableZoneLogic::ROW_COUNT; ++visualRow) {
- // Table logical grid Y: 0 = Command, 1 = Support, 2 = Front.
+ // Logical grid Y: 0 = Front, 1 = Support, 2 = Command.
const int row = inverted ? TableZoneLogic::ROW_COUNT - 1 - visualRow : visualRow;
QString label;
QColor color;
switch (row) {
case 0:
- label = tr("COMMAND ZONE");
- color = commandZoneColor;
+ label = tr("FRONT ZONE");
+ color = frontZoneColor;
break;
case 1:
label = tr("SUPPORT ZONE");
color = supportZoneColor;
break;
default:
- label = tr("FRONT ZONE");
- color = frontZoneColor;
+ label = tr("COMMAND ZONE");
+ color = commandZoneColor;
break;
}
diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp
index 829a8ac42..43b5f2e83 100644
--- a/cockatrice/src/main.cpp
+++ b/cockatrice/src/main.cpp
@@ -238,11 +238,13 @@ int main(int argc, char *argv[])
app.addLibraryPath(app.applicationDirPath() + "/plugins");
#endif
- // These values are only used by the settings loader/saver
- // Wrong or outdated values are kept to not break things
- QCoreApplication::setOrganizationName("Cockatrice");
- QCoreApplication::setOrganizationDomain("cockatrice.github.io");
- QCoreApplication::setApplicationName("Cockatrice");
+ // Continuumatrice must never use Cockatrice's profile. These identifiers are
+ // set before SettingsCache is initialized, so Qt's AppLocalDataLocation,
+ // CacheLocation and legacy QSettings migration use independent paths.
+ // Keep the application name stable: changing it again would move profiles.
+ QCoreApplication::setOrganizationName("Continuum Universes Studio");
+ QCoreApplication::setOrganizationDomain("continuum-universes.com");
+ QCoreApplication::setApplicationName("Continuumatrice");
QCoreApplication::setApplicationVersion(VERSION_STRING);
#ifdef Q_OS_MAC
@@ -336,7 +338,7 @@ int main(int argc, char *argv[])
// Command-line parser
QCommandLineParser parser;
- parser.setApplicationDescription("Cockatrice Client");
+ parser.setApplicationDescription("Continuumatrice Client");
parser.addHelpOption();
parser.addVersionOption();
diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp
index 08294d72c..693ce96da 100644
--- a/cockatrice/src/single_instance_manager.cpp
+++ b/cockatrice/src/single_instance_manager.cpp
@@ -27,7 +27,7 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend)
{
// Scope the socket name to the current user so one user's instance cannot
// hijack another user's session.
- serverName = scopedLocalServerName(QStringLiteral("CockatriceSingleInstance"));
+ serverName = scopedLocalServerName(QStringLiteral("ContinuumatriceSingleInstance"));
// Hand off to an already-running primary instance if one exists. Never steal
// the socket of a busy primary: it is alive and will act on the payload.
diff --git a/doc/continuumatrice-battlefield.md b/doc/continuumatrice-battlefield.md
index 130dc1f2d..9b4fc5b37 100644
--- a/doc/continuumatrice-battlefield.md
+++ b/doc/continuumatrice-battlefield.md
@@ -19,7 +19,7 @@ Support = #A0286981
Command = #A05E428C
```
-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 row order follows the player's mirrored/inverted layout, so Front always faces the battlefield center.
+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.
## Font
@@ -29,4 +29,4 @@ The font is also detected if already installed as "Continuumorphous Regular" or
## Compatibility
-The existing drag/drop, row mapping, stacking, card counters, playmats, and multiplayer protocols are preserved. This does **not** enforce gameplay placement restrictions.
+The existing drag/drop, stacking, card counters, playmats, and multiplayer protocols are preserved. The automatic `tablerow` placement is explicitly mapped `0 -> Front`, `1 -> Support`, `2 -> Command` (not Cockatrice's reversed Magic-oriented mapping). This does **not** enforce gameplay placement restrictions.
diff --git a/doc/continuumatrice-card-data.md b/doc/continuumatrice-card-data.md
new file mode 100644
index 000000000..8e3f9a12f
--- /dev/null
+++ b/doc/continuumatrice-card-data.md
@@ -0,0 +1,105 @@
+# Continuumatrice: isolated profile and card database
+
+Continuumatrice intentionally uses its own Qt application identity:
+- Application: `Continuumatrice`
+- Organization: `Continuum Universes Studio`
+- Organization domain: `continuum-universes.com`
+
+The identity is assigned in `cockatrice/src/main.cpp` **before** `SettingsCache` runs.
+The single-instance local socket is also renamed so a running Cockatrice client does not intercept Continuumatrice launches.
+
+## Default Linux paths
+
+For the usual openSUSE / XDG environment (`XDG_DATA_HOME` and `XDG_CACHE_HOME` unset):
+
+| Purpose | Default |
+| --- | --- |
+| Application data | `~/.local/share/Continuumatrice/` |
+| Preferences | `~/.local/share/Continuumatrice/settings/` |
+| Primary database | `~/.local/share/Continuumatrice/cards.xml` |
+| Additional custom XML databases | `~/.local/share/Continuumatrice/customsets/` |
+| Decks | `~/.local/share/Continuumatrice/decks/` |
+| Downloaded/local images | `~/.local/share/Continuumatrice/pics/` |
+| Local custom images | `~/.local/share/Continuumatrice/pics/CUSTOM/` |
+| User themes | `~/.local/share/Continuumatrice/themes/` |
+| Cache | `~/.cache/Continuumatrice/` |
+
+Qt follows the XDG environment variables if set; the paths above are defaults, not mandatory constants. A `portable.dat` next to the binary activates Cockatrice's existing portable mode and stores data/cache next to the executable instead. No old Cockatrice data is automatically copied or deleted.
+
+**Note:** The client's executable and file-association/URL scheme are still called Cockatrice. This patch isolates profiles, not every branding or installer integration. Avoid running the upstream Oracle Magic-card downloader against this profile until you intentionally configure it; that is a separate task.
+
+## Card database format
+
+Continuumatrice currently reads **Cockatrice XML v4** (schema `doc/carddatabase_v4/cards.xsd`). This is a database description and card list, not the rendered card images. You do not need to build new parsing logic to playtest your own cards.
+
+A self-contained *test-only* main database `cards.xml` can look like:
+
+```xml
+
+
+
+
+ CTEST
+ Continuumatrice Test Cards
+
+
+
+
+ Example Front Card
+ Test card for checking battlefield placement.
+
+ Unit
+
+ CTEST
+ 0
+
+
+ Example Support Card
+
+ Unit
+
+ CTEST
+ 1
+
+
+ Example Command Card
+
+ Unit
+
+ CTEST
+ 2
+
+
+
+```
+
+These names are examples, **not** Continuum Universes lore or playable canon.
+
+For a real card catalogue, generate XML from the card design/export pipeline and store it under `cards.xml` (main database), or under `customsets/*.xml` (additional sets). **A readable main `cards.xml` is needed for the loader to proceed to the optional custom sets**, so do not use custom sets exclusively without one. The loader collects custom-set files recursively and loads them in alphabetical order.
+
+The XML `` maps to:
+- `0` = Front
+- `1` = Support
+- `2` = Command
+
+By default, cards are manually positioned and moved: this client does not evaluate card rules.
+
+## Images
+
+Card images are separate from card metadata:
+- Put a local image in `pics/CUSTOM/` with the **same corrected card name** as its XML name (such as `Example Front Card.png`). Custom pictures are indexed recursively. Or
+- Provide a `picurl` URL attribute on the printing's `` tag and let the client download it.
+
+For reliable matching across reprints, the importer also recognizes names incorporating the set code, collector number and printing/provider ID. The existing loader supports various filename schemes and file extensions.
+
+## Loading and testing
+
+1. Update and rebuild the fork.
+2. Launch Continuumatrice and verify its `Continuumatrice` folder is created.
+3. Quit the client before writing a new `cards.xml` or image files.
+4. Place your own XML and images in the new profile.
+5. Relaunch or reload the card database in the client.
+6. Ensure your set is enabled; test dragging cards and checking Front/Support/Command placement.
+7. Check normal Cockatrice's profile is still intact and can open separately.
+
+**Magic Set Editor** is an editor for card layouts and card content, but its native project format is not Cockatrice XML v4. You will need a matching export script/template or a conversion pipeline that writes this XML and the image files.
diff --git a/doc/continuumatrice-special-piles.md b/doc/continuumatrice-special-piles.md
new file mode 100644
index 000000000..64de4e480
--- /dev/null
+++ b/doc/continuumatrice-special-piles.md
@@ -0,0 +1,54 @@
+# Limbo, Refuge and Oblivion piles
+
+Continuumatrice adds **three persistent public card piles per player** alongside the existing Deck, Hand, Graveyard, Exile, Sideboard, Table and Stack zones.
+
+| Display name | Stable wire-zone name | Visibility | Interactions |
+| --- | --- | --- | --- |
+| Limbo | `limbo` | Public | Drop cards, move selected cards, view contents |
+| Refuge | `refuge` | Public | Drop cards, move selected cards, view contents |
+| Oblivion | `oblivion` | Public | Drop cards, move selected cards, view contents |
+
+These use existing `PileZoneLogic`, `PileZone`, server `Server_CardZone` and string-based protocol zone names. **The piles do not enforce gameplay rules** (e.g., what gets sent to Limbo or whether Oblivion is permanent). Nothing automatically sends cards to them; users move cards through drag/drop or the card **Move to** menu.
+
+The three new piles appear in a second, compact column, aligned with the existing deck, graveyard and exile piles. The table and hand shift to allow the extra column without overlap. Double-click a new pile to open the standard zone viewer; right-click for a **View** menu. The card count and the pile name/icon are shown even when the pile is nonempty.
+
+## Icons and asset overrides
+
+The fork bundles original fallback SVG icons:
+
+- `cockatrice/resources/piles/limbo.svg`
+- `cockatrice/resources/piles/refuge.svg`
+- `cockatrice/resources/piles/oblivion.svg`
+
+They are registered in `cockatrice/cockatrice.qrc`; you do not need to manually copy them to run the built client.
+
+To assign your own art **without recompiling**, put a matching asset in the **currently selected Continuumatrice theme**:
+
+```text
+~/.local/share/Continuumatrice/themes//piles/
+ limbo.svg
+ refuge.svg
+ oblivion.svg
+```
+
+The theme can use `.png`, `.jpg`, `.jpeg` or `.svg`, and the existing theme manager also supports scheme-specific file variants such as `limbo-dark.svg`. The theme's asset takes precedence; the bundled SVG is a fallback. Reload/change the theme to refresh the icons. The icons are cosmetic; all card data remains in the database and the runtime zone state.
+
+## Multiplayer compatibility
+
+The new piles are created in **both** client-side `PlayerLogic` and server-side `Server_Player::setupZones`. They therefore work with the fork's **local game** and a **Continuumatrice-built server**. An unmodified upstream Cockatrice server does not create these zones and may reject card moves to `limbo`, `refuge` or `oblivion`. Use a matched client/server build for multiplayer.
+
+No new protobuf messages are needed; the existing `Command_MoveCard` uses zone-name strings. Older upstream clients do not have dedicated icons/menus for these piles.
+
+## Manual verification (after rebuild)
+
+1. Launch a local game with two players and confirm all three new piles appear on each side.
+2. Confirm the new icons and labels are distinct and the board does not overlap the old piles, stack, hand or table.
+3. Move one card from the hand/table to each pile using **Move to**, and then by dragging.
+4. Double-click or right-click each pile to view its card list, then move a card back to the table or hand.
+5. Check the pile counters and name badges are legible when cards are present.
+6. Rejoin/reconnect a game (and try a replay) to ensure pile contents are reconstructed from server zones.
+7. Test both mirrored player orientations and the horizontal-hand setting at different window sizes.
+8. Override an icon with an asset in the selected theme and reload the theme.
+9. If using a remote server, ensure that server also has this fork's zone definitions.
+
+Build locally with `cmake --build build --target cockatrice --parallel 16`. This change has **not** been compiled or visually run in this environment; the compilation and runtime checks above are essential before merging.
diff --git a/libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h b/libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h
index e5c4cd84e..24e2dabcb 100644
--- a/libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h
+++ b/libcockatrice_game/libcockatrice/game/player/card_menu_action_type.h
@@ -22,7 +22,11 @@ enum CardMenuActionType
cmMoveToHand,
cmMoveToGraveyard,
cmMoveToExile,
- cmMoveToTable
+ cmMoveToTable,
+ // Continuumatrice piles: append to preserve existing action values.
+ cmMoveToLimbo,
+ cmMoveToRefuge,
+ cmMoveToOblivion
};
#endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H
diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp b/libcockatrice_game/libcockatrice/game/player/player_actions.cpp
index 90823fec6..966b145af 100644
--- a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp
+++ b/libcockatrice_game/libcockatrice/game/player/player_actions.cpp
@@ -1974,6 +1974,23 @@ void PlayerActions::cardMenuAction(QList selectedCards, CardMenuAct
commandList.append(cmd);
break;
}
+ case cmMoveToLimbo:
+ case cmMoveToRefuge:
+ case cmMoveToOblivion: {
+ auto *cmd = new Command_MoveCard;
+ cmd->set_start_player_id(startPlayerId);
+ cmd->set_start_zone(startZone.toStdString());
+ cmd->mutable_cards_to_move()->CopyFrom(idList);
+ cmd->set_target_player_id(player->getPlayerInfo()->getId());
+ const char *targetZone = type == cmMoveToLimbo ? ZoneNames::LIMBO
+ : type == cmMoveToRefuge ? ZoneNames::REFUGE
+ : ZoneNames::OBLIVION;
+ cmd->set_target_zone(targetZone);
+ cmd->set_x(0);
+ cmd->set_y(0);
+ commandList.append(cmd);
+ break;
+ }
case cmMoveToTable: {
// Each card needs its own command because table row, pt, and cipt vary per card
for (const auto &card : cardList) {
diff --git a/libcockatrice_game/libcockatrice/game/player/player_logic.cpp b/libcockatrice_game/libcockatrice/game/player/player_logic.cpp
index 96bd07164..4d00a2843 100644
--- a/libcockatrice_game/libcockatrice/game/player/player_logic.cpp
+++ b/libcockatrice_game/libcockatrice/game/player/player_logic.cpp
@@ -54,6 +54,9 @@ void PlayerLogic::initializeZones()
addZone(new PileZoneLogic(this, ZoneNames::DECK, false, true, false, this));
addZone(new PileZoneLogic(this, ZoneNames::GRAVE, false, false, true, this));
addZone(new PileZoneLogic(this, ZoneNames::EXILE, false, false, true, this));
+ addZone(new PileZoneLogic(this, ZoneNames::LIMBO, false, false, true, this));
+ addZone(new PileZoneLogic(this, ZoneNames::REFUGE, false, false, true, this));
+ addZone(new PileZoneLogic(this, ZoneNames::OBLIVION, false, false, true, this));
addZone(new PileZoneLogic(this, ZoneNames::SIDEBOARD, false, false, false, this));
addZone(new TableZoneLogic(this, ZoneNames::TABLE, true, false, true, this));
addZone(new StackZoneLogic(this, ZoneNames::STACK, true, false, true, this));
@@ -111,6 +114,7 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
{
static QSet builtinZones{/* PileZones */
ZoneNames::DECK, ZoneNames::GRAVE, ZoneNames::EXILE, ZoneNames::SIDEBOARD,
+ ZoneNames::LIMBO, ZoneNames::REFUGE, ZoneNames::OBLIVION,
/* TableZone */
ZoneNames::TABLE,
/* StackZone */
diff --git a/libcockatrice_game/libcockatrice/game/player/player_logic.h b/libcockatrice_game/libcockatrice/game/player/player_logic.h
index d6c4f5187..758bae237 100644
--- a/libcockatrice_game/libcockatrice/game/player/player_logic.h
+++ b/libcockatrice_game/libcockatrice/game/player/player_logic.h
@@ -178,6 +178,21 @@ public:
return qobject_cast(zones.value(ZoneNames::EXILE));
}
+ PileZoneLogic *getLimboZone()
+ {
+ return qobject_cast(zones.value(ZoneNames::LIMBO));
+ }
+
+ PileZoneLogic *getRefugeZone()
+ {
+ return qobject_cast(zones.value(ZoneNames::REFUGE));
+ }
+
+ PileZoneLogic *getOblivionZone()
+ {
+ return qobject_cast(zones.value(ZoneNames::OBLIVION));
+ }
+
PileZoneLogic *getSideboardZone()
{
return qobject_cast(zones.value(ZoneNames::SIDEBOARD));
diff --git a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp b/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp
index 834794268..f1a896ed2 100644
--- a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp
+++ b/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.cpp
@@ -52,5 +52,6 @@ int TableZoneLogic::tableRowToGridY(int tableRow)
if (tableRow > 2) {
tableRow = 1;
}
- return clampValidTableRow(2 - tableRow);
+ // Continuumatrice rows are ordered Front (0), Support (1), Command (2).
+ return clampValidTableRow(tableRow);
}
diff --git a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h b/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h
index c828d8b91..c60a49be4 100644
--- a/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h
+++ b/libcockatrice_game/libcockatrice/game/zones/table_zone_logic.h
@@ -29,7 +29,7 @@ public:
bool _contentsKnown,
QObject *parent = nullptr);
- /** @brief Number of rows in the table zone grid (0=creatures, 1=noncreatures, 2=lands). */
+ /** @brief Number of rows in the table zone grid (0=Front, 1=Support, 2=Command). */
static const int ROW_COUNT = 3;
/**
@@ -38,9 +38,9 @@ public:
static int clampValidTableRow(const int row);
/**
- * Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands)
- * to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g.,
- * instants/sorceries) default to the noncreatures row.
+ * Converts a card's logical table row (0=Front, 1=Support, 2=Command)
+ * to the corresponding grid Y coordinate. Rows greater than 2
+ * fall back to the middle (Support) row.
*/
static int tableRowToGridY(int tableRow);
diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp
index b40dae09b..b8ee7c6c2 100644
--- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp
+++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp
@@ -105,6 +105,9 @@ void Server_Player::setupZones()
addZone(new Server_CardZone(this, ZoneNames::STACK, false, ServerInfo_Zone::PublicZone));
addZone(new Server_CardZone(this, ZoneNames::GRAVE, false, ServerInfo_Zone::PublicZone));
addZone(new Server_CardZone(this, ZoneNames::EXILE, false, ServerInfo_Zone::PublicZone));
+ addZone(new Server_CardZone(this, ZoneNames::LIMBO, false, ServerInfo_Zone::PublicZone));
+ addZone(new Server_CardZone(this, ZoneNames::REFUGE, false, ServerInfo_Zone::PublicZone));
+ addZone(new Server_CardZone(this, ZoneNames::OBLIVION, false, ServerInfo_Zone::PublicZone));
addCounter(new Server_Counter(0, "life", makeColor(255, 255, 255), 25, game->getStartingLifeTotal()));
addCounter(new Server_Counter(1, "w", makeColor(255, 255, 150), 20, 0));
diff --git a/libcockatrice_utility/libcockatrice/utility/zone_names.h b/libcockatrice_utility/libcockatrice/utility/zone_names.h
index d1463de6a..8adc427d7 100644
--- a/libcockatrice_utility/libcockatrice/utility/zone_names.h
+++ b/libcockatrice_utility/libcockatrice/utility/zone_names.h
@@ -9,6 +9,9 @@ namespace ZoneNames
constexpr const char *TABLE = "table";
constexpr const char *GRAVE = "grave";
constexpr const char *EXILE = "rfg"; // "removed from game"
+constexpr const char *LIMBO = "limbo";
+constexpr const char *REFUGE = "refuge";
+constexpr const char *OBLIVION = "oblivion";
constexpr const char *HAND = "hand";
constexpr const char *DECK = "deck";
constexpr const char *SIDEBOARD = "sb";