mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-13 05:20:00 -08:00
Add sixteen server-synchronized Essence resource pools per match player
This commit is contained in:
parent
00a7feff25
commit
a969e4622c
12 changed files with 466 additions and 13 deletions
|
|
@ -72,3 +72,9 @@ This is an independent fork, **not** an official Cockatrice release.
|
|||
This repository includes the full dedicated multiplayer server, **Continuum-Servatrice**, built with the `continuum-servatrice` CMake target (default `WITH_SERVER=ON`). Its build output is `build/continuum-servatrice/continuum-servatrice`. Use the standalone `continuum_servatrice` MySQL database and the provided server-specific SQL schema/config; do not reuse MTG Servatrice's database or tables.
|
||||
|
||||
[Dedicated server hosting and database instructions](doc/continuum-servatrice-hosting.md)
|
||||
|
||||
## In-match Essence pools
|
||||
|
||||
Each player has a server-authoritative **16-type Essence pool** (`M S O G E A Q C J B R L V T P N`), starting at zero. The match displays all balances beside the battlefield with quick gain/spend/set controls. Essence isn't generated by drawing land cards, and there is no automatic per-turn resource refill or cost spending until actual CCG gameplay rules define those mechanics. Continuum-Servatrice and the local server both use the same counter protocol and state.
|
||||
|
||||
[In-match Essence resource pools and test checklist](doc/continuumatrice-match-essence.md)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ set(cockatrice_SOURCES
|
|||
src/game_graphics/tally/stats_tally.cpp
|
||||
src/game_graphics/tally/subtype_tally.cpp
|
||||
src/game_graphics/tally/tally.cpp
|
||||
src/game_graphics/player/essence_pool_item.cpp
|
||||
src/game_graphics/player/player_area.cpp
|
||||
src/game_graphics/player/player_dialogs.cpp
|
||||
src/game_graphics/player/player_graphics_item.cpp
|
||||
|
|
|
|||
186
cockatrice/src/game_graphics/player/essence_pool_item.cpp
Normal file
186
cockatrice/src/game_graphics/player/essence_pool_item.cpp
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#include "essence_pool_item.h"
|
||||
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QGraphicsSceneHoverEvent>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QInputDialog>
|
||||
#include <QPainter>
|
||||
#include <QPen>
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/game/board/counter_state.h>
|
||||
#include <libcockatrice/game/player/player_actions.h>
|
||||
#include <libcockatrice/game/player/player_info.h>
|
||||
#include <libcockatrice/game/player/player_logic.h>
|
||||
#include <libcockatrice/utility/essence_counter.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
EssencePoolItem::EssencePoolItem(PlayerLogic *_player, QGraphicsItem *parent)
|
||||
: QGraphicsObject(parent), player(_player)
|
||||
{
|
||||
setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
|
||||
setAcceptHoverEvents(true);
|
||||
setZValue(5);
|
||||
|
||||
connect(player, &PlayerLogic::counterAdded, this, &EssencePoolItem::attachCounter);
|
||||
connect(player, &PlayerLogic::counterRemoved, this, &EssencePoolItem::detachCounter);
|
||||
|
||||
// Reconnect and late view construction must paint the full snapshot,
|
||||
// even when the counterAdded events happened before this item existed.
|
||||
for (CounterState *state : player->getCounters()) {
|
||||
attachCounter(state);
|
||||
}
|
||||
setToolTip(tr("Essence pool — left click +1, right click -1, Shift+left to set"));
|
||||
}
|
||||
|
||||
QRectF EssencePoolItem::boundingRect() const
|
||||
{
|
||||
return QRectF(0, 0, Width, Height);
|
||||
}
|
||||
|
||||
void EssencePoolItem::attachCounter(CounterState *state)
|
||||
{
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const QChar code = EssenceCounter::codeFromName(state->getName());
|
||||
if (code.isNull()) {
|
||||
return;
|
||||
}
|
||||
counters.insert(code, state);
|
||||
connect(state, &CounterState::valueChanged, this, [this](int, int, bool) { update(); });
|
||||
update();
|
||||
}
|
||||
|
||||
void EssencePoolItem::detachCounter(int id)
|
||||
{
|
||||
for (auto it = counters.begin(); it != counters.end();) {
|
||||
if (it.value() && it.value()->getId() == id) {
|
||||
it = counters.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
CounterState *EssencePoolItem::stateFor(QChar code) const
|
||||
{
|
||||
return counters.value(code).data();
|
||||
}
|
||||
|
||||
QChar EssencePoolItem::codeAt(const QPointF &position) const
|
||||
{
|
||||
const qreal x = position.x() - Padding;
|
||||
const qreal y = position.y() - HeaderHeight;
|
||||
if (x < 0 || y < 0 || x >= Columns * CellWidth || y >= 4 * CellHeight) {
|
||||
return QChar();
|
||||
}
|
||||
const int col = static_cast<int>(x) / CellWidth;
|
||||
const int row = static_cast<int>(y) / CellHeight;
|
||||
const int index = row * Columns + col;
|
||||
const QString &codes = EssenceCounter::symbols();
|
||||
return index >= 0 && index < codes.size() ? codes.at(index) : QChar();
|
||||
}
|
||||
|
||||
void EssencePoolItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
|
||||
{
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setPen(QPen(QColor(118, 125, 158), 1));
|
||||
painter->setBrush(QColor(16, 19, 31, 232));
|
||||
painter->drawRoundedRect(boundingRect().adjusted(0.5, 0.5, -0.5, -0.5), 7, 7);
|
||||
|
||||
QFont titleFont = painter->font();
|
||||
titleFont.setBold(true);
|
||||
titleFont.setPixelSize(15);
|
||||
painter->setFont(titleFont);
|
||||
painter->setPen(QColor(236, 238, 248));
|
||||
painter->drawText(QRectF(8, 3, Width - 16, 24), Qt::AlignCenter, tr("ESSENCE POOL"));
|
||||
|
||||
QFont countFont = painter->font();
|
||||
countFont.setBold(true);
|
||||
countFont.setPixelSize(14);
|
||||
painter->setFont(countFont);
|
||||
|
||||
const QString &codes = EssenceCounter::symbols();
|
||||
for (int i = 0; i < codes.size(); ++i) {
|
||||
const QChar code = codes.at(i);
|
||||
const int col = i % Columns;
|
||||
const int row = i / Columns;
|
||||
const qreal x = Padding + col * CellWidth;
|
||||
const qreal y = HeaderHeight + row * CellHeight;
|
||||
const QRectF tile(x, y, CellWidth - 3, CellHeight - 3);
|
||||
|
||||
painter->setPen(QPen(QColor(85, 91, 119), 1));
|
||||
painter->setBrush(QColor(43, 47, 67, 228));
|
||||
painter->drawRoundedRect(tile, 5, 5);
|
||||
|
||||
const QPixmap icon = ManaSymbolPixmapGenerator::generatePixmap(QString(code), QSize(32, 32));
|
||||
if (!icon.isNull()) {
|
||||
painter->drawPixmap(QPointF(x + 9, y + 2), icon);
|
||||
} else {
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(QRectF(x, y + 2, CellWidth - 3, 30), Qt::AlignCenter, QString(code));
|
||||
}
|
||||
|
||||
CounterState *state = stateFor(code);
|
||||
const int count = state ? state->getValue() : 0;
|
||||
painter->setPen(count > 0 ? QColor(255, 244, 191) : QColor(178, 182, 200));
|
||||
painter->drawText(QRectF(x, y + 33, CellWidth - 3, 18),
|
||||
Qt::AlignCenter, QString::number(count));
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
void EssencePoolItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
const QChar code = codeAt(event->pos());
|
||||
CounterState *state = stateFor(code);
|
||||
if (!state || !player->getPlayerInfo()->getLocalOrJudge()) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
PlayerActions *actions = player->getPlayerActions();
|
||||
if (event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier)) {
|
||||
const QString label = code == QChar('N') ? tr("Colorless (N)") :
|
||||
code == QChar('J') ? tr("Jasper (J)") : tr("Essence %1").arg(code);
|
||||
bool accepted = false;
|
||||
QWidget *parent = event->widget() ? event->widget()->window() : nullptr;
|
||||
const int value = QInputDialog::getInt(parent, tr("Set Essence"),
|
||||
tr("Available %1:").arg(label),
|
||||
state->getValue(), 0, std::numeric_limits<int>::max(), 1, &accepted);
|
||||
if (accepted) {
|
||||
actions->actSetCounter(state->getId(), value);
|
||||
}
|
||||
} else if (event->button() == Qt::LeftButton) {
|
||||
actions->actIncrementCounter(state->getId(), 1);
|
||||
} else if (event->button() == Qt::RightButton) {
|
||||
if (state->getValue() > 0) {
|
||||
actions->actIncrementCounter(state->getId(), -1);
|
||||
}
|
||||
} else {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void EssencePoolItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
|
||||
{
|
||||
const QChar code = codeAt(event->pos());
|
||||
CounterState *state = stateFor(code);
|
||||
if (!state) {
|
||||
setToolTip(tr("Essence pool — all types start at zero"));
|
||||
return;
|
||||
}
|
||||
const QString name = code == QChar('N') ? tr("Colorless Essence") :
|
||||
code == QChar('J') ? tr("Jasper Essence") : tr("Essence %1").arg(code);
|
||||
setToolTip(tr("%1: %2 available\nLeft +1 | Right -1 | Shift+Left: Set")
|
||||
.arg(name).arg(state->getValue()));
|
||||
}
|
||||
57
cockatrice/src/game_graphics/player/essence_pool_item.h
Normal file
57
cockatrice/src/game_graphics/player/essence_pool_item.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#ifndef CONTINUUMATRICE_ESSENCE_POOL_ITEM_H
|
||||
#define CONTINUUMATRICE_ESSENCE_POOL_ITEM_H
|
||||
|
||||
#include <QGraphicsObject>
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
|
||||
class CounterState;
|
||||
class PlayerLogic;
|
||||
class QGraphicsSceneMouseEvent;
|
||||
class QGraphicsSceneHoverEvent;
|
||||
class QPainter;
|
||||
class QStyleOptionGraphicsItem;
|
||||
class QWidget;
|
||||
|
||||
/**
|
||||
* An interactive, sixteen-type Essence resource pool attached to a player
|
||||
* battlefield. All values are read from the existing server-owned CounterState
|
||||
* objects; this graphics item does NOT own mutable match resource balances.
|
||||
*
|
||||
* Left click +1, right click -1, Shift+left to set an exact nonnegative amount.
|
||||
* Only the owning player or a game judge can modify the pool.
|
||||
*/
|
||||
class EssencePoolItem final : public QGraphicsObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static constexpr int Width = 224;
|
||||
static constexpr int Height = 258;
|
||||
|
||||
explicit EssencePoolItem(PlayerLogic *player, QGraphicsItem *parent = nullptr);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
|
||||
private:
|
||||
static constexpr int Columns = 4;
|
||||
static constexpr int Padding = 8;
|
||||
static constexpr int CellWidth = 52;
|
||||
static constexpr int CellHeight = 54;
|
||||
static constexpr int HeaderHeight = 29;
|
||||
|
||||
[[nodiscard]] QChar codeAt(const QPointF &position) const;
|
||||
[[nodiscard]] CounterState *stateFor(QChar code) const;
|
||||
void attachCounter(CounterState *state);
|
||||
void detachCounter(int id);
|
||||
|
||||
PlayerLogic *player;
|
||||
QHash<QChar, QPointer<CounterState>> counters;
|
||||
};
|
||||
|
||||
#endif // CONTINUUMATRICE_ESSENCE_POOL_ITEM_H
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
#include "libcockatrice/utility/card_ref.h"
|
||||
#include "menu/player_menu.h"
|
||||
#include "player_area.h"
|
||||
#include "essence_pool_item.h"
|
||||
#include "player_dialogs.h"
|
||||
#include "player_target.h"
|
||||
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
#include <libcockatrice/game/player/player_actions.h>
|
||||
#include <libcockatrice/game/player/player_info.h>
|
||||
#include <libcockatrice/game/player/player_logic.h>
|
||||
#include <libcockatrice/utility/essence_counter.h>
|
||||
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||
#include <libcockatrice/game/zones/pile_zone_logic.h>
|
||||
|
|
@ -51,7 +53,10 @@
|
|||
|
||||
namespace
|
||||
{
|
||||
constexpr qreal EXTRA_PILE_COLUMN_WIDTH = CardDimensions::HEIGHT_F + 24;
|
||||
// Reserve a third, compact column for the synchronized 4x4 Essence pool.
|
||||
// This keeps the 16 resource balances visible without obscuring the table.
|
||||
constexpr qreal EXTRA_PILE_COLUMN_WIDTH =
|
||||
CardDimensions::HEIGHT_F + 24 + EssencePoolItem::Width + 16;
|
||||
} // namespace
|
||||
|
||||
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||
|
|
@ -103,6 +108,13 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
initializeZones();
|
||||
|
||||
// Board-owned renderer only. All Essence balances live in player runtime
|
||||
// CounterState and are sent/validated by the server.
|
||||
essencePool = new EssencePoolItem(player, this);
|
||||
const qreal essenceLeft = CardDimensions::HEIGHT_F + counterAreaWidth + 15 + 8 +
|
||||
CardDimensions::HEIGHT_F + 14;
|
||||
essencePool->setPos(essenceLeft, 14);
|
||||
|
||||
connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this,
|
||||
&PlayerGraphicsItem::onCustomZoneAdded);
|
||||
|
||||
|
|
@ -310,6 +322,11 @@ void PlayerGraphicsItem::setMirrored(bool _mirrored)
|
|||
|
||||
void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
||||
{
|
||||
// The Essence pool owns its own compact 4x4 display. Never add sixteen
|
||||
// full-size generic counters down the narrow life-counter column.
|
||||
if (EssenceCounter::isEssenceCounter(state->getName())) {
|
||||
return;
|
||||
}
|
||||
AbstractCounter *widget;
|
||||
if (state->getName() == "life") {
|
||||
widget = playerTarget->addCounter(state);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class TableZone;
|
|||
class AbstractCounter;
|
||||
class CardZone;
|
||||
class CounterState;
|
||||
class EssencePoolItem;
|
||||
class PlayerArea;
|
||||
class PlayerLogic;
|
||||
struct CardRef;
|
||||
|
|
@ -160,6 +161,7 @@ private:
|
|||
PlayerDialogs *playerDialogs;
|
||||
PlayerArea *playerArea;
|
||||
PlayerTarget *playerTarget;
|
||||
EssencePoolItem *essencePool = nullptr;
|
||||
QMap<int, AbstractCounter *> counterWidgets;
|
||||
QMap<QString, CardZone *> zoneGraphicsItems;
|
||||
PileZone *deckZoneGraphicsItem;
|
||||
|
|
|
|||
83
doc/continuumatrice-match-essence.md
Normal file
83
doc/continuumatrice-match-essence.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Match Essence pools (Continuum Universes CCG)
|
||||
|
||||
Continuumatrice matches provide **16 independent Essence balances per player**, using the canonical sequence:
|
||||
|
||||
```text
|
||||
M S O G E A Q C J B R L V T P N
|
||||
```
|
||||
|
||||
**J** is Jasper; **N** is colorless. These are not Magic mana colors, and Essence is not primarily obtained by drawing land cards.
|
||||
|
||||
## Behavior
|
||||
|
||||
The client displays a compact **4 × 4 Essence Pool** in a reserved column beside each player's battlefield. The panel shows the user's installed `icons/essence/<code>.svg` artwork and the current quantity, including zero.
|
||||
|
||||
- **Left-click:** add one Essence of that type (gain).
|
||||
- **Right-click:** remove one Essence of that type (spend), when the balance is positive.
|
||||
- **Shift + left-click:** set an exact nonnegative balance through a number dialog.
|
||||
- Other players and spectators can **view** the balances; only the player controlling the pool (or a judge) can modify it.
|
||||
|
||||
For example, generating three Jasper Essence from a card ability currently means clicking `J` three times or setting its balance. Likewise, paying two Jasper means decrementing its balance twice. Future automated card effects should call a validated resource transaction action rather than simulating mouse clicks.
|
||||
|
||||
### What this change does **not** assume
|
||||
|
||||
It does not invent Essence generation rules, a land-based resource curve, turn-based Essence resets, automatic cost deduction, overdraft/Essence debt, or activation triggers. The core match tracks available resources, but **gameplay effects are still manual**. Define those rules separately before implementing them.
|
||||
|
||||
## Server-authoritative state
|
||||
|
||||
The existing Cockatrice-derived counter protocol already supports all the required functionality. The fork's shared `EssenceCounter` helper (`libcockatrice_utility/libcockatrice/utility/essence_counter.h`) defines stable counter names and the canonical symbol order.
|
||||
|
||||
The server creates sixteen counters at game setup with IDs `1..16` and names `essence:M`, `essence:S`, ..., `essence:N`. The existing life counter retains ID `0`. These names are protocol/storage identifiers; client gameplay displays the corresponding Essence symbols rather than exposing raw IDs.
|
||||
|
||||
Resource counters:
|
||||
- Start at `0`, independently for every player.
|
||||
- Cannot go below `0` (validated by the server for increment **and** set commands).
|
||||
- Cannot be deleted or created a second time via the generic counter commands.
|
||||
- Are serialized to client snapshots, replay events, and reconnect state by the existing counter protocol.
|
||||
- Do not live on card ScriptableObjects, card metadata/XML, or a global static client singleton.
|
||||
|
||||
`Server_Player` implements this for both the self-hosted **Continuum-Servatrice** and the embedded/local game server because they share the same player server code. **Both client and server must use a matching Continuumatrice build**. The regular MTG Cockatrice client/server do not provide the native Essence Pool display.
|
||||
|
||||
## Implementation map
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `libcockatrice_utility/libcockatrice/utility/essence_counter.h` | Canonical Essence order, protocol names, nonnegative values |
|
||||
| `libcockatrice_card/libcockatrice/card/essence_types.h` | Card-cost parser uses the same source of symbols |
|
||||
| `libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp` | Server counter initialization, updates, permanent-counter restrictions |
|
||||
| `cockatrice/src/game_graphics/player/essence_pool_item.h/.cpp` | Compact graphical resource display, manual controls, server-state observation |
|
||||
| `cockatrice/src/game_graphics/player/player_graphics_item.cpp` | Places Essence pool beside player's board and excludes it from the narrow ordinary-counter stack |
|
||||
| `tests/essence_types_test.cpp` | Protocol-name round trips and nonnegative value utility checks |
|
||||
|
||||
The panel never owns the true resource quantities. It listens for `CounterState::valueChanged` and sends normal `actIncrementCounter`/`actSetCounter` commands through `PlayerActions`. Only the server authoritatively changes the stored balance, so remote clients and replays see the same values.
|
||||
|
||||
## Build and tests
|
||||
|
||||
On the `feature/continuumatrice-essence-match-pools` branch:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -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
|
||||
```
|
||||
|
||||
## Functional acceptance checklist
|
||||
|
||||
- [ ] In a new local game, each player has a 4×4 pool with exactly 16 Essence types and zero balances.
|
||||
- [ ] Gaining `J` leaves `C`, `B`, and `N` unchanged; right-clicking `J` at zero cannot make it negative.
|
||||
- [ ] Shift+left can set `N` (colorless) to a specific nonnegative amount.
|
||||
- [ ] The normal life counter still works; no default MTG W/U/B/R/G/X/Storm counters appear.
|
||||
- [ ] Both players see the same balance after a command is processed by the server.
|
||||
- [ ] A spectator can view but not modify another player's Essence.
|
||||
- [ ] Disconnect/reconnect restores all 16 values from the game-state snapshot.
|
||||
- [ ] Replay playback restores previously recorded Essence changes.
|
||||
- [ ] A remote game running Continuum-Servatrice matches the local game behavior.
|
||||
- [ ] The Essence panel does not overlap the three battlefield bands or Limbo/Refuge/Oblivion piles at normal zoom.
|
||||
|
||||
**Current status:** Source implementation committed. This server/client change has **not** been compiled or tested in a live match yet. The new browser and match UI need user-side smoke testing.
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QRegularExpressionMatchIterator>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/utility/essence_counter.h>
|
||||
|
||||
namespace Essence
|
||||
{
|
||||
|
|
@ -13,8 +14,7 @@ namespace Essence
|
|||
// The other codes have no names, colors or game mechanics assigned here.
|
||||
inline const QString &symbols()
|
||||
{
|
||||
static const QString value = QStringLiteral("MSOGEAQCJBRLVTPN");
|
||||
return value;
|
||||
return EssenceCounter::symbols();
|
||||
}
|
||||
|
||||
inline bool isSymbol(const QChar symbol)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/utility/color.h>
|
||||
#include <libcockatrice/utility/essence_counter.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <qalgorithms.h>
|
||||
|
|
@ -110,13 +111,16 @@ void Server_Player::setupZones()
|
|||
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));
|
||||
addCounter(new Server_Counter(2, "u", makeColor(150, 150, 255), 20, 0));
|
||||
addCounter(new Server_Counter(3, "b", makeColor(150, 150, 150), 20, 0));
|
||||
addCounter(new Server_Counter(4, "r", makeColor(250, 150, 150), 20, 0));
|
||||
addCounter(new Server_Counter(5, "g", makeColor(150, 255, 150), 20, 0));
|
||||
addCounter(new Server_Counter(6, "x", makeColor(255, 255, 255), 20, 0));
|
||||
addCounter(new Server_Counter(7, "storm", makeColor(255, 150, 30), 20, 0));
|
||||
// Continuum Universes CCG: every player has an independent sixteen-type
|
||||
// Essence pool. It starts at zero and can change without drawing lands.
|
||||
// These counters are the authoritative game-state source: the existing
|
||||
// counter protocol automatically handles spectators, replays and reconnect.
|
||||
// Stable ids 1..16 correspond to M,S,O,G,E,A,Q,C,J,B,R,L,V,T,P,N.
|
||||
int essenceId = 1;
|
||||
for (const QChar code : EssenceCounter::symbols()) {
|
||||
addCounter(new Server_Counter(essenceId++, EssenceCounter::name(code),
|
||||
makeColor(168, 172, 189), 18, 0));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
|
|
@ -480,7 +484,14 @@ Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
bool didChange = c->incrementCount(cmd.delta());
|
||||
const int oldValue = c->getCount();
|
||||
// Essence is an available-resource balance, never a debt counter.
|
||||
// Leave ordinary player counters unchanged.
|
||||
(void)c->incrementCount(cmd.delta());
|
||||
if (EssenceCounter::isEssenceCounter(c->getName()) && c->getCount() < 0) {
|
||||
(void)c->setCount(EssenceCounter::clampBalance(c->getCount()));
|
||||
}
|
||||
const bool didChange = c->getCount() != oldValue;
|
||||
if (didChange) {
|
||||
Event_SetCounter event;
|
||||
event.set_counter_id(c->getId());
|
||||
|
|
@ -501,7 +512,13 @@ Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContai
|
|||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
auto *c = new Server_Counter(newCounterId(), nameFromStdString(cmd.counter_name()), cmd.counter_color(),
|
||||
const QString requestedName = nameFromStdString(cmd.counter_name());
|
||||
// The canonical pool is unique, present for every player, and may not
|
||||
// be forged or replaced using a generic "create counter" command.
|
||||
if (EssenceCounter::isEssenceCounter(requestedName)) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
auto *c = new Server_Counter(newCounterId(), requestedName, cmd.counter_color(),
|
||||
cmd.radius(), cmd.value());
|
||||
addCounter(c);
|
||||
|
||||
|
|
@ -533,7 +550,8 @@ Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
bool didChange = c->setCount(cmd.value());
|
||||
const int newValue = EssenceCounter::isEssenceCounter(c->getName()) ? EssenceCounter::clampBalance(cmd.value()) : cmd.value();
|
||||
const bool didChange = c->setCount(newValue);
|
||||
if (didChange) {
|
||||
Event_SetCounter event;
|
||||
event.set_counter_id(c->getId());
|
||||
|
|
@ -559,6 +577,10 @@ Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &
|
|||
if (!counter) {
|
||||
return Response::RespNameNotFound;
|
||||
}
|
||||
// Canonical Essence counters are permanent for the duration of a match.
|
||||
if (EssenceCounter::isEssenceCounter(counter->getName())) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
counters.remove(counterId);
|
||||
delete counter;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ set(UTILITY_HEADERS
|
|||
libcockatrice/utility/cryptoutil.h
|
||||
libcockatrice/utility/debouncer.h
|
||||
libcockatrice/utility/expression.h
|
||||
libcockatrice/utility/essence_counter.h
|
||||
libcockatrice/utility/installer_close_protocol.h
|
||||
libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/local_server_name.h
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
#ifndef CONTINUUMATRICE_ESSENCE_COUNTER_H
|
||||
#define CONTINUUMATRICE_ESSENCE_COUNTER_H
|
||||
|
||||
#include <QChar>
|
||||
#include <QString>
|
||||
|
||||
|
||||
namespace EssenceCounter
|
||||
{
|
||||
// A single authoritative resource-code order shared by card metadata, the
|
||||
// client match display, and the dedicated/local multiplayer servers.
|
||||
// This is a protocol identity list, not a definition of gameplay generation.
|
||||
inline const QString &symbols()
|
||||
{
|
||||
static const QString value = QStringLiteral("MSOGEAQCJBRLVTPN");
|
||||
return value;
|
||||
}
|
||||
|
||||
inline bool isSymbol(QChar code)
|
||||
{
|
||||
return symbols().contains(code);
|
||||
}
|
||||
|
||||
// Names are serialized by Cockatrice's existing ServerInfo_Counter protocol.
|
||||
// Keep these stable for replays, game snapshots, and reconnects.
|
||||
inline QString name(QChar code)
|
||||
{
|
||||
return isSymbol(code) ? QStringLiteral("essence:") + code : QString();
|
||||
}
|
||||
|
||||
inline QChar codeFromName(const QString &counterName)
|
||||
{
|
||||
constexpr qsizetype prefixSize = 8; // "essence:"
|
||||
if (counterName.size() != prefixSize + 1 ||
|
||||
!counterName.startsWith(QStringLiteral("essence:"))) {
|
||||
return QChar();
|
||||
}
|
||||
const QChar code = counterName.at(prefixSize);
|
||||
return isSymbol(code) ? code : QChar();
|
||||
}
|
||||
|
||||
// Resource balances cannot be negative. The caller handles overflow-safe
|
||||
// addition separately; this function only clamps the final amount.
|
||||
inline int clampBalance(int amount)
|
||||
{
|
||||
return amount < 0 ? 0 : amount;
|
||||
}
|
||||
|
||||
inline bool isEssenceCounter(const QString &counterName)
|
||||
{
|
||||
return !codeFromName(counterName).isNull();
|
||||
}
|
||||
} // namespace EssenceCounter
|
||||
|
||||
#endif // CONTINUUMATRICE_ESSENCE_COUNTER_H
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/essence_types.h>
|
||||
#include <libcockatrice/utility/essence_counter.h>
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
|
@ -15,6 +16,28 @@ TEST(EssenceTypesTest, HasExactlySixteenCanonicalSymbols)
|
|||
EXPECT_FALSE(Essence::isSymbol(QChar('U'))); // MTG blue is not Essence.
|
||||
}
|
||||
|
||||
TEST(EssenceTypesTest, NetworkCounterNamesHaveStableRoundTrip)
|
||||
{
|
||||
EXPECT_EQ(Essence::symbols(), EssenceCounter::symbols());
|
||||
int position = 1; // Counter id 0 is the existing life counter.
|
||||
for (const QChar code : EssenceCounter::symbols()) {
|
||||
const QString networkName = EssenceCounter::name(code);
|
||||
EXPECT_TRUE(networkName.startsWith(QStringLiteral("essence:")));
|
||||
EXPECT_EQ(EssenceCounter::codeFromName(networkName), code);
|
||||
EXPECT_TRUE(EssenceCounter::isEssenceCounter(networkName));
|
||||
++position;
|
||||
}
|
||||
EXPECT_EQ(position, 17);
|
||||
EXPECT_TRUE(EssenceCounter::name(QChar('W')).isEmpty());
|
||||
EXPECT_FALSE(EssenceCounter::isEssenceCounter(QStringLiteral("essence:W")));
|
||||
EXPECT_FALSE(EssenceCounter::isEssenceCounter(QStringLiteral("essence:MM")));
|
||||
EXPECT_FALSE(EssenceCounter::isEssenceCounter(QStringLiteral("life")));
|
||||
EXPECT_EQ(EssenceCounter::clampBalance(-1), 0);
|
||||
EXPECT_EQ(EssenceCounter::clampBalance(-900), 0);
|
||||
EXPECT_EQ(EssenceCounter::clampBalance(0), 0);
|
||||
EXPECT_EQ(EssenceCounter::clampBalance(19), 19);
|
||||
}
|
||||
|
||||
TEST(EssenceTypesTest, ParsesAllBracedAndUnbracedTypes)
|
||||
{
|
||||
const QString raw = QStringLiteral("{M}{S}{O}{G}{E}{A}{Q}{C}{J}{B}{R}{L}{V}{T}{P}{N}");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue