Merge pull request #3 from Continuum-Universes-Studio/feature/continuumatrice-zone-bands

Continuumatrice: Limbo, Refuge and Oblivion card piles
This commit is contained in:
Christian Daniel Jensen 2026-11-02 02:04:54 -07:00 • committed by GitHub
commit 070c455b16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 390 additions and 27 deletions

View file

@ -5,6 +5,9 @@
<file>resources/cockatrice-logo-white.svg</file>
<file>resources/hand.svg</file>
<file>resources/hr.jpg</file>
<file>resources/piles/limbo.svg</file>
<file>resources/piles/refuge.svg</file>
<file>resources/piles/oblivion.svg</file>
<file>resources/icons/arrow_bottom_green.svg</file>
<file>resources/icons/arrow_down_green.svg</file>

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="28" fill="#15182C" stroke="#A3A6EE" stroke-width="3"/>
<circle cx="32" cy="32" r="20" fill="none" stroke="#8076D5" stroke-width="4"/>
<path d="M13 32c12-18 26-18 38 0-12 18-26 18-38 0Z" fill="none" stroke="#E4D9FF" stroke-width="3"/>
<circle cx="32" cy="32" r="6" fill="#D7C9FF"/>
<path d="M32 3v7M32 54v7M3 32h7M54 32h7" stroke="#C9CAFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="27" fill="#27131F" stroke="#EC899E" stroke-width="3"/>
<path d="M32 9a23 23 0 1 0 23 23c-9 5-21 1-25-10-2-5-2-9 2-13Z" fill="#B85C77"/>
<circle cx="32" cy="32" r="12" fill="#080B17"/>
<circle cx="32" cy="32" r="5" fill="#080B17" stroke="#F5C2CE" stroke-width="2"/>
<path d="M15 13 10 7M49 51l6 6" stroke="#E9A1B3" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M32 4 56 14v18c0 16-11 24-24 29C19 56 8 48 8 32V14Z" fill="#143F47" stroke="#8BE7D1" stroke-width="3"/>
<path d="m17 31 15-14 15 14" fill="none" stroke="#E8FFF4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 30v17h22V30" fill="none" stroke="#98E5D3" stroke-width="4" stroke-linejoin="round"/>
<path d="M29 47V35h6v12" fill="#C8FFF0"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View file

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

View file

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

View file

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

View file

@ -20,12 +20,14 @@
#include "tally_menu.h"
#include "utility_menu.h"
#include <QAction>
#include <QMenu>
#include <QString>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/utility/zone_names.h>
PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player)
{
@ -45,6 +47,19 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_
graveMenu = addManagedMenu<GraveyardMenu>(player, playerMenu);
rfgMenu = addManagedMenu<RfgMenu>(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<SideboardMenu>(player, playerMenu);
customZonesMenu = addManagedMenu<CustomZoneMenu>(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);

View file

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

View file

@ -49,6 +49,11 @@
#include <qnamespace.h>
#include <qnumeric.h>
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(),

View file

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

View file

@ -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 <QApplication>
#include <QColor>
#include <QFont>
#include <QPixmap>
#include <QString>
#include <QCursor>
#include <QGraphicsItem>
#include <QGraphicsSceneMouseEvent>
@ -28,12 +33,19 @@
#include <qnamespace.h>
#include <string>
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/<name> 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<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &)

View file

@ -9,6 +9,8 @@
#include "card_zone.h"
#include <QPixmap>
#include <QString>
#include <qtmetamacros.h>
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;

View file

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

View file

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

View file

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

View file

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

View file

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<cockatrice_carddatabase version="4">
<sets>
<set>
<name>CTEST</name>
<longname>Continuumatrice Test Cards</longname>
</set>
</sets>
<cards>
<card>
<name>Example Front Card</name>
<text>Test card for checking battlefield placement.</text>
<prop>
<type>Unit</type>
</prop>
<set num="001" rarity="common">CTEST</set>
<tablerow>0</tablerow>
</card>
<card>
<name>Example Support Card</name>
<prop>
<type>Unit</type>
</prop>
<set num="002">CTEST</set>
<tablerow>1</tablerow>
</card>
<card>
<name>Example Command Card</name>
<prop>
<type>Unit</type>
</prop>
<set num="003">CTEST</set>
<tablerow>2</tablerow>
</card>
</cards>
</cockatrice_carddatabase>
```
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 `<tablerow>` 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 `<set>` 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.

View file

@ -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/<YourTheme>/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.

View file

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

View file

@ -1974,6 +1974,23 @@ void PlayerActions::cardMenuAction(QList<CardState *> 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) {

View file

@ -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<QString> builtinZones{/* PileZones */
ZoneNames::DECK, ZoneNames::GRAVE, ZoneNames::EXILE, ZoneNames::SIDEBOARD,
ZoneNames::LIMBO, ZoneNames::REFUGE, ZoneNames::OBLIVION,
/* TableZone */
ZoneNames::TABLE,
/* StackZone */

View file

@ -178,6 +178,21 @@ public:
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::EXILE));
}
PileZoneLogic *getLimboZone()
{
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::LIMBO));
}
PileZoneLogic *getRefugeZone()
{
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::REFUGE));
}
PileZoneLogic *getOblivionZone()
{
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::OBLIVION));
}
PileZoneLogic *getSideboardZone()
{
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::SIDEBOARD));

View file

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

View file

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

View file

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

View file

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