Support creating face-down tokens (#5800)

* add new fields to proto

* update token dlg

* send facedown in command

* update server to get it to work

* disable certain edits when face down

* update client event processing

* log face-down token creation

* Don't support colors on face-down tokens

The other client doesn't know about the color, so it causes a desync

* Update wording

Co-authored-by: Basile Clement <Elarnon@users.noreply.github.com>

* Allow annotations on face-down tokens

---------

Co-authored-by: Basile Clement <Elarnon@users.noreply.github.com>
This commit is contained in:
RickyRister 2025-04-27 21:30:23 -07:00 committed by GitHub
parent e3465be8c1
commit bb8213deb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 97 additions and 19 deletions

View file

@ -63,6 +63,9 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
destroyCheckBox = new QCheckBox(tr("&Destroy token when it leaves the table")); destroyCheckBox = new QCheckBox(tr("&Destroy token when it leaves the table"));
destroyCheckBox->setChecked(true); destroyCheckBox->setChecked(true);
faceDownCheckBox = new QCheckBox(tr("Create face-down (Only hides name)"));
connect(faceDownCheckBox, &QCheckBox::toggled, this, &DlgCreateToken::faceDownCheckBoxToggled);
QGridLayout *grid = new QGridLayout; QGridLayout *grid = new QGridLayout;
grid->addWidget(nameLabel, 0, 0); grid->addWidget(nameLabel, 0, 0);
grid->addWidget(nameEdit, 0, 1); grid->addWidget(nameEdit, 0, 1);
@ -73,6 +76,7 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
grid->addWidget(annotationLabel, 3, 0); grid->addWidget(annotationLabel, 3, 0);
grid->addWidget(annotationEdit, 3, 1); grid->addWidget(annotationEdit, 3, 1);
grid->addWidget(destroyCheckBox, 4, 0, 1, 2); grid->addWidget(destroyCheckBox, 4, 0, 1, 2);
grid->addWidget(faceDownCheckBox, 5, 0, 1, 2);
QGroupBox *tokenDataGroupBox = new QGroupBox(tr("Token data")); QGroupBox *tokenDataGroupBox = new QGroupBox(tr("Token data"));
tokenDataGroupBox->setLayout(grid); tokenDataGroupBox->setLayout(grid);
@ -155,6 +159,21 @@ void DlgCreateToken::closeEvent(QCloseEvent *event)
SettingsCache::instance().setTokenDialogGeometry(saveGeometry()); SettingsCache::instance().setTokenDialogGeometry(saveGeometry());
} }
void DlgCreateToken::faceDownCheckBoxToggled(bool checked)
{
if (checked) {
colorEdit->setCurrentIndex(6);
colorEdit->setEnabled(false);
ptEdit->clear();
ptEdit->clearFocus();
ptEdit->setEnabled(false);
} else {
colorEdit->setEnabled(true);
ptEdit->setEnabled(true);
annotationEdit->setEnabled(true);
}
}
void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QModelIndex & /*previous*/) void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QModelIndex & /*previous*/)
{ {
const QModelIndex realIndex = cardDatabaseDisplayModel->mapToSource(current); const QModelIndex realIndex = cardDatabaseDisplayModel->mapToSource(current);
@ -230,5 +249,6 @@ TokenInfo DlgCreateToken::getTokenInfo() const
.color = colorEdit->itemData(colorEdit->currentIndex()).toString(), .color = colorEdit->itemData(colorEdit->currentIndex()).toString(),
.pt = ptEdit->text(), .pt = ptEdit->text(),
.annotation = annotationEdit->text(), .annotation = annotationEdit->text(),
.destroy = destroyCheckBox->isChecked()}; .destroy = destroyCheckBox->isChecked(),
.faceDown = faceDownCheckBox->isChecked()};
} }

View file

@ -24,6 +24,7 @@ struct TokenInfo
QString pt; QString pt;
QString annotation; QString annotation;
bool destroy = true; bool destroy = true;
bool faceDown = false;
}; };
class DlgCreateToken : public QDialog class DlgCreateToken : public QDialog
@ -36,6 +37,7 @@ public:
protected: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
private slots: private slots:
void faceDownCheckBoxToggled(bool checked);
void tokenSelectionChanged(const QModelIndex &current, const QModelIndex &previous); void tokenSelectionChanged(const QModelIndex &current, const QModelIndex &previous);
void updateSearch(const QString &search); void updateSearch(const QString &search);
void actChooseTokenFromAll(bool checked); void actChooseTokenFromAll(bool checked);
@ -51,6 +53,7 @@ private:
QComboBox *colorEdit; QComboBox *colorEdit;
QLineEdit *nameEdit, *ptEdit, *annotationEdit; QLineEdit *nameEdit, *ptEdit, *annotationEdit;
QCheckBox *destroyCheckBox; QCheckBox *destroyCheckBox;
QCheckBox *faceDownCheckBox;
QRadioButton *chooseTokenFromAllRadioButton, *chooseTokenFromDeckRadioButton; QRadioButton *chooseTokenFromAllRadioButton, *chooseTokenFromDeckRadioButton;
CardInfoPictureWidget *pic; CardInfoPictureWidget *pic;
QTreeView *chooseTokenView; QTreeView *chooseTokenView;

View file

@ -1855,6 +1855,7 @@ void Player::actCreateAnotherToken()
cmd.set_pt(lastTokenInfo.pt.toStdString()); cmd.set_pt(lastTokenInfo.pt.toStdString());
cmd.set_annotation(lastTokenInfo.annotation.toStdString()); cmd.set_annotation(lastTokenInfo.annotation.toStdString());
cmd.set_destroy_on_zone_change(lastTokenInfo.destroy); cmd.set_destroy_on_zone_change(lastTokenInfo.destroy);
cmd.set_face_down(lastTokenInfo.faceDown);
cmd.set_x(-1); cmd.set_x(-1);
cmd.set_y(lastTokenTableRow); cmd.set_y(lastTokenTableRow);
@ -2233,10 +2234,10 @@ void Player::eventCreateToken(const Event_CreateToken &event)
CardItem *card = new CardItem(this, nullptr, QString::fromStdString(event.card_name()), CardItem *card = new CardItem(this, nullptr, QString::fromStdString(event.card_name()),
QString::fromStdString(event.card_provider_id()), event.card_id()); QString::fromStdString(event.card_provider_id()), event.card_id());
// use db PT if not provided in event // use db PT if not provided in event and not face-down
if (!QString::fromStdString(event.pt()).isEmpty()) { if (!QString::fromStdString(event.pt()).isEmpty()) {
card->setPT(QString::fromStdString(event.pt())); card->setPT(QString::fromStdString(event.pt()));
} else { } else if (!event.face_down()) {
CardInfoPtr dbCard = card->getInfo(); CardInfoPtr dbCard = card->getInfo();
if (dbCard) { if (dbCard) {
card->setPT(dbCard->getPowTough()); card->setPT(dbCard->getPowTough());
@ -2245,8 +2246,9 @@ void Player::eventCreateToken(const Event_CreateToken &event)
card->setColor(QString::fromStdString(event.color())); card->setColor(QString::fromStdString(event.color()));
card->setAnnotation(QString::fromStdString(event.annotation())); card->setAnnotation(QString::fromStdString(event.annotation()));
card->setDestroyOnZoneChange(event.destroy_on_zone_change()); card->setDestroyOnZoneChange(event.destroy_on_zone_change());
card->setFaceDown(event.face_down());
emit logCreateToken(this, card->getName(), card->getPT()); emit logCreateToken(this, card->getName(), card->getPT(), card->getFaceDown());
zone->addCard(card, true, event.x(), event.y()); zone->addCard(card, true, event.x(), event.y());
} }

View file

@ -128,7 +128,7 @@ signals:
Player *targetPlayer, Player *targetPlayer,
QString targetCard, QString targetCard,
bool _playerTarget); bool _playerTarget);
void logCreateToken(Player *player, QString cardName, QString pt); void logCreateToken(Player *player, QString cardName, QString pt, bool faceDown);
void logDrawCards(Player *player, int number, bool deckIsEmpty); void logDrawCards(Player *player, int number, bool deckIsEmpty);
void logUndoDraw(Player *player, QString cardName); void logUndoDraw(Player *player, QString cardName);
void logMoveCard(Player *player, CardItem *card, CardZone *startZone, int oldX, CardZone *targetZone, int newX); void logMoveCard(Player *player, CardItem *card, CardZone *startZone, int oldX, CardZone *targetZone, int newX);

View file

@ -214,12 +214,16 @@ void MessageLogWidget::logCreateArrow(Player *player,
} }
} }
void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt) void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt, bool faceDown)
{ {
appendHtmlServerMessage(tr("%1 creates token: %2%3.") if (faceDown) {
.arg(sanitizeHtml(player->getName())) appendHtmlServerMessage(tr("%1 creates a face down token.").arg(sanitizeHtml(player->getName())));
.arg(cardLink(std::move(cardName))) } else {
.arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt)))); appendHtmlServerMessage(tr("%1 creates token: %2%3.")
.arg(sanitizeHtml(player->getName()))
.arg(cardLink(std::move(cardName)))
.arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt))));
}
} }
void MessageLogWidget::logDeckSelect(Player *player, QString deckHash, int sideboardSize) void MessageLogWidget::logDeckSelect(Player *player, QString deckHash, int sideboardSize)

View file

@ -41,7 +41,7 @@ public slots:
Player *targetPlayer, Player *targetPlayer,
QString targetCard, QString targetCard,
bool playerTarget); bool playerTarget);
void logCreateToken(Player *player, QString cardName, QString pt); void logCreateToken(Player *player, QString cardName, QString pt, bool faceDown);
void logDeckSelect(Player *player, QString deckHash, int sideboardSize); void logDeckSelect(Player *player, QString deckHash, int sideboardSize);
void logDestroyCard(Player *player, QString cardName); void logDestroyCard(Player *player, QString cardName);
void logDrawCards(Player *player, int number, bool deckIsEmpty); void logDrawCards(Player *player, int number, bool deckIsEmpty);

View file

@ -27,4 +27,5 @@ message Command_CreateToken {
optional TargetMode target_mode = 11; optional TargetMode target_mode = 11;
optional string card_provider_id = 12; optional string card_provider_id = 12;
optional bool face_down = 13;
} }

View file

@ -15,4 +15,5 @@ message Event_CreateToken {
optional sint32 x = 8; optional sint32 x = 8;
optional sint32 y = 9; optional sint32 y = 9;
optional string card_provider_id = 10; optional string card_provider_id = 10;
optional bool face_down = 11;
} }

View file

@ -386,13 +386,23 @@ void Server_Player::revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorag
} }
} }
static Event_CreateToken makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord) /**
* Creates the create token event.
* By default, will set event's name and color fields to empty if the token is face-down
*/
static Event_CreateToken
makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, bool revealFacedownInfo = false)
{ {
Event_CreateToken event; Event_CreateToken event;
event.set_zone_name(zone->getName().toStdString()); event.set_zone_name(zone->getName().toStdString());
event.set_card_id(card->getId()); event.set_card_id(card->getId());
event.set_card_name(card->getName().toStdString()); event.set_face_down(card->getFaceDown());
event.set_card_provider_id(card->getProviderId().toStdString());
if (!card->getFaceDown() || revealFacedownInfo) {
event.set_card_name(card->getName().toStdString());
event.set_card_provider_id(card->getProviderId().toStdString());
}
event.set_color(card->getColor().toStdString()); event.set_color(card->getColor().toStdString());
event.set_pt(card->getPT().toStdString()); event.set_pt(card->getPT().toStdString());
event.set_annotation(card->getAnnotation().toStdString()); event.set_annotation(card->getAnnotation().toStdString());
@ -401,7 +411,6 @@ static Event_CreateToken makeCreateTokenEvent(Server_CardZone *zone, Server_Card
event.set_y(yCoord); event.set_y(yCoord);
return event; return event;
} }
static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Card *parentCard = nullptr) static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Card *parentCard = nullptr)
{ {
Event_AttachCard event; Event_AttachCard event;
@ -1494,7 +1503,8 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer
const QString cardName = nameFromStdString(cmd.card_name()); const QString cardName = nameFromStdString(cmd.card_name());
const QString cardProviderId = nameFromStdString(cmd.card_provider_id()); const QString cardProviderId = nameFromStdString(cmd.card_provider_id());
if (zone->hasCoords()) { if (zone->hasCoords()) {
xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, false); bool dontStackSameName = cmd.face_down();
xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, dontStackSameName);
} }
if (xCoord < 0) { if (xCoord < 0) {
xCoord = 0; xCoord = 0;
@ -1505,13 +1515,17 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer
auto *card = new Server_Card(cardName, cardProviderId, newCardId(), xCoord, yCoord); auto *card = new Server_Card(cardName, cardProviderId, newCardId(), xCoord, yCoord);
card->moveToThread(thread()); card->moveToThread(thread());
card->setPT(nameFromStdString(cmd.pt())); // Client should already prevent face-down tokens from having attributes; this just an extra server-side check
card->setColor(nameFromStdString(cmd.color())); if (!cmd.face_down()) {
card->setColor(nameFromStdString(cmd.color()));
card->setPT(nameFromStdString(cmd.pt()));
}
card->setAnnotation(nameFromStdString(cmd.annotation())); card->setAnnotation(nameFromStdString(cmd.annotation()));
card->setDestroyOnZoneChange(cmd.destroy_on_zone_change()); card->setDestroyOnZoneChange(cmd.destroy_on_zone_change());
card->setFaceDown(cmd.face_down());
zone->insertCard(card, xCoord, yCoord); zone->insertCard(card, xCoord, yCoord);
ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId); sendCreateTokenEvents(zone, card, xCoord, yCoord, ges);
// check if the token is a replacement for an existing card // check if the token is a replacement for an existing card
if (!targetCard) { if (!targetCard) {
@ -1642,6 +1656,38 @@ Server_Player::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer
return Response::RespOk; return Response::RespOk;
} }
/**
* Creates and sends the events required to properly communicate the given token creation.
* Primarily written to handle creating face-down tokens.
*/
void Server_Player::sendCreateTokenEvents(Server_CardZone *zone,
Server_Card *card,
int xCoord,
int yCoord,
GameEventStorage &ges)
{
// Token is not face-down; things are easy
if (!card->getFaceDown()) {
ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId);
return;
}
// Token is face-down. We have to send different info to each player
auto eventOthers = makeCreateTokenEvent(zone, card, xCoord, yCoord, false);
ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers);
auto eventPrivate = makeCreateTokenEvent(zone, card, xCoord, yCoord, true);
ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId);
// Event_CreateToken didn't use to have face_down field; send attribute event afterward for backwards compatibility
Event_SetCardAttr event;
event.set_zone_name(zone->getName().toStdString());
event.set_card_id(card->getId());
event.set_attribute(AttrFaceDown);
event.set_attr_value("1");
ges.enqueueGameEvent(event, playerId);
}
Response::ResponseCode Response::ResponseCode
Server_Player::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) Server_Player::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
{ {

View file

@ -84,6 +84,7 @@ private:
bool conceded; bool conceded;
bool sideboardLocked; bool sideboardLocked;
void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges); void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges);
void sendCreateTokenEvents(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, GameEventStorage &ges);
public: public:
mutable QMutex playerMutex; mutable QMutex playerMutex;