This commit is contained in:
2026-06-18 17:29:28 +08:00
commit 08a070d874
720 changed files with 41946 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
#ifndef CARDDATA_H
#define CARDDATA_H
#include <QString>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
#include <QPixmap>
#include <QMap>
#include <QVector>
struct CardDef {
QString id;
QString name;
int mp = 0;
int priority = 0;
int count = 0;
QString winCondition;
QString effectDescription;
QString effectTrigger;
bool needsTarget = false;
QString targetType;
bool unusable = false;
static CardDef fromJson(const QJsonObject& obj) {
CardDef def;
def.id = obj["id"].toString();
def.name = obj["name"].toString();
def.mp = obj["mp"].toInt();
def.priority = obj["priority"].toInt();
def.count = obj["count"].toInt();
def.winCondition = obj["win_condition"].toString();
def.effectDescription = obj["effect_description"].toString();
def.effectTrigger = obj["effect_trigger"].toString();
def.needsTarget = obj["needs_target"].toBool();
def.targetType = obj["target_type"].toString();
def.unusable = obj["unusable"].toBool(false);
return def;
}
};
struct GameSettingDef {
int playerCount = 0;
int harmonyTarget = 0;
QVector<QPair<QString, int>> removeCards;
int totalCards = 0;
int handSize = 0;
static GameSettingDef fromJson(const QJsonObject& obj) {
GameSettingDef def;
def.playerCount = obj["player_count"].toInt();
def.harmonyTarget = obj["harmony_target"].toInt();
def.totalCards = obj["total_cards"].toInt();
def.handSize = obj["hand_size"].toInt();
for (const auto& v : obj["remove_cards"].toArray()) {
auto o = v.toObject();
def.removeCards.append({o["id"].toString(), o["count"].toInt()});
}
return def;
}
};
struct CardInstance {
QString uid;
QString typeId;
bool faceUp = false;
bool selected = false;
bool enabled = true;
bool locked = false;
};
class CardDatabase {
public:
static CardDatabase& instance() {
static CardDatabase db;
return db;
}
bool loadFromFile(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return false;
auto doc = QJsonDocument::fromJson(file.readAll());
if (doc.isNull())
return false;
auto root = doc.object();
for (const auto& v : root["cards"].toArray()) {
auto def = CardDef::fromJson(v.toObject());
_cardDefs[def.id] = def;
}
for (const auto& v : root["game_settings"].toArray()) {
auto setting = GameSettingDef::fromJson(v.toObject());
_gameSettings[setting.playerCount] = setting;
}
return true;
}
const CardDef* getCardDef(const QString& id) const {
auto it = _cardDefs.find(id);
return it != _cardDefs.end() ? &it.value() : nullptr;
}
QMap<QString, CardDef> allCardDefs() const { return _cardDefs; }
const GameSettingDef* getGameSetting(int playerCount) const {
auto it = _gameSettings.find(playerCount);
return it != _gameSettings.end() ? &it.value() : nullptr;
}
QPixmap getCardFrontImage(const QString& typeId) const {
return QPixmap(QString(":/images/%1").arg(typeId));
}
QPixmap getCardBackImage() const {
return QPixmap(":/images/card_back");
}
private:
CardDatabase() = default;
QMap<QString, CardDef> _cardDefs;
QMap<int, GameSettingDef> _gameSettings;
};
#endif // CARDDATA_H
+20
View File
@@ -0,0 +1,20 @@
#ifndef PLAYERDATA_H
#define PLAYERDATA_H
#include <QString>
#include <QVector>
struct PlayerData {
QString playerId;
QString nickname;
bool isReady = false;
bool isHost = false;
bool isExited = false;
int handCardCount = 0;
QVector<QString> handCardIds;
QVector<QString> skillZoneCardIds;
QVector<QString> challengeZoneCardIds;
QString lastCardId;
};
#endif // PLAYERDATA_H
+44
View File
@@ -0,0 +1,44 @@
#ifndef ROOMDATA_H
#define ROOMDATA_H
#include <QString>
#include <QVector>
#include "PlayerData.h"
enum class RoomState {
Waiting,
Setting,
Playing,
Settling,
Finished
};
enum class GamePhase {
None,
Dealing,
WaitingForCardSelect,
WaitingForAction,
WaitingForTarget,
EffectResolving,
WaitingForResponse,
TurnEndCheck,
HarmonyJudge,
ChallengeJudge,
VictoryJudge
};
struct RoomData {
QString roomId;
QString roomName;
RoomState state = RoomState::Waiting;
GamePhase phase = GamePhase::None;
int maxPlayers = 6;
int harmonyTarget = 0;
bool chaosMode = false;
QString currentTurnPlayerId;
QVector<PlayerData> players;
QVector<QString> harmonyZoneCardIds;
int turnTimeLimit = 60;
};
#endif // ROOMDATA_H
+169
View File
@@ -0,0 +1,169 @@
#include "NetworkManager.h"
#include <QJsonArray>
#include <QDateTime>
NetworkManager::NetworkManager(QObject* parent)
: QObject(parent)
{
connect(&_socket, &QWebSocket::connected, this, &NetworkManager::onConnected);
connect(&_socket, &QWebSocket::disconnected, this, &NetworkManager::onDisconnected);
connect(&_socket, &QWebSocket::textMessageReceived, this, &NetworkManager::onTextMessageReceived);
connect(&_socket, &QWebSocket::errorOccurred, this, &NetworkManager::onError);
connect(&_socket, &QWebSocket::binaryMessageReceived, this, [this](const QByteArray& msg) {
emit binaryFrameReceived(msg);
});
connect(&_heartbeatTimer, &QTimer::timeout, this, &NetworkManager::onHeartbeat);
}
void NetworkManager::connectToServer(const QString& url) {
_serverUrl = url;
_socket.open(QUrl(url));
}
void NetworkManager::disconnect() {
_heartbeatTimer.stop();
_socket.close();
}
bool NetworkManager::isConnected() const {
return _socket.state() == QAbstractSocket::ConnectedState;
}
void NetworkManager::sendMessage(const QString& type, const QJsonObject& payload) {
QJsonObject msg;
msg["type"] = type;
msg["seq"] = ++_seqNum;
msg["timestamp"] = QDateTime::currentMSecsSinceEpoch();
if (!payload.isEmpty())
msg["payload"] = payload;
_socket.sendTextMessage(QJsonDocument(msg).toJson(QJsonDocument::Compact));
}
void NetworkManager::joinRoom(const QString& roomId, const QString& nickname) {
QJsonObject payload;
payload["room_id"] = roomId;
payload["nickname"] = nickname;
sendMessage("join_room", payload);
}
void NetworkManager::createRoom(const QString& nickname, int maxPlayers) {
QJsonObject payload;
payload["nickname"] = nickname;
payload["max_players"] = maxPlayers;
sendMessage("create_room", payload);
}
void NetworkManager::leaveRoom() {
sendMessage("leave_room");
}
void NetworkManager::setReady(bool ready) {
QJsonObject payload;
payload["ready"] = ready;
sendMessage("set_ready", payload);
}
void NetworkManager::startGame() {
sendMessage("start_game");
}
void NetworkManager::selectCard(const QString& cardUid) {
QJsonObject payload;
payload["card_uid"] = cardUid;
sendMessage("select_card", payload);
}
void NetworkManager::confirmAction(const QString& action, const QJsonObject& extra) {
QJsonObject payload;
payload["action"] = action;
if (!extra.isEmpty()) {
for (auto it = extra.begin(); it != extra.end(); ++it)
payload[it.key()] = it.value();
}
sendMessage("confirm_action", payload);
}
void NetworkManager::effectResponse(const QJsonObject& response) {
sendMessage("effect_response", response);
}
void NetworkManager::listRooms() {
sendMessage("list_rooms");
}
void NetworkManager::sendChat(const QString& text) {
QJsonObject payload;
payload["text"] = text;
sendMessage("send_chat", payload);
}
void NetworkManager::sendBinaryFrame(const QByteArray& data) {
if (_socket.state() == QAbstractSocket::ConnectedState)
_socket.sendBinaryMessage(data);
}
void NetworkManager::onConnected() {
_heartbeatTimer.start(15000);
emit connected();
}
void NetworkManager::onDisconnected() {
_heartbeatTimer.stop();
emit disconnected();
}
void NetworkManager::onTextMessageReceived(const QString& message) {
auto doc = QJsonDocument::fromJson(message.toUtf8());
if (doc.isNull()) return;
handleServerMessage(doc.object());
}
void NetworkManager::onError(QAbstractSocket::SocketError error) {
Q_UNUSED(error)
emit connectionError(_socket.errorString());
}
void NetworkManager::onHeartbeat() {
sendMessage("heartbeat");
}
void NetworkManager::handleServerMessage(const QJsonObject& msg) {
auto type = msg["type"].toString();
auto payload = msg["payload"].toObject();
if (type == "room_state") {
emit roomStateUpdated(payload);
} else if (type == "game_start") {
emit gameStarted(payload);
} else if (type == "state_snapshot") {
emit gameStateSnapshot(payload);
} else if (type == "card_move") {
emit cardMoveEvent(payload);
} else if (type == "card_flip") {
emit cardFlipEvent(payload);
} else if (type == "effect_trigger") {
emit effectTriggered(payload);
} else if (type == "request_choice") {
emit requestChoice(payload);
} else if (type == "player_exit") {
emit playerExited(payload);
} else if (type == "settlement") {
emit settlementNotify(payload);
} else if (type == "game_end") {
emit gameEnded(payload);
} else if (type == "error") {
emit errorNotify(payload);
} else if (type == "chat") {
emit chatMessage(payload);
} else if (type == "room_created") {
emit roomCreated(payload["room_id"].toString());
} else if (type == "room_joined") {
emit roomJoined(payload);
} else if (type == "room_left") {
emit roomLeft();
} else if (type == "room_list") {
emit roomListReceived(payload["rooms"].toArray());
} else if (type == "settlement_result") {
emit settlementResult(payload);
}
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef NETWORKMANAGER_H
#define NETWORKMANAGER_H
#include <QObject>
#include <QWebSocket>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QTimer>
class NetworkManager : public QObject {
Q_OBJECT
public:
static NetworkManager& instance() {
static NetworkManager mgr;
return mgr;
}
void connectToServer(const QString& url);
void disconnect();
bool isConnected() const;
void sendMessage(const QString& type, const QJsonObject& payload = {});
void joinRoom(const QString& roomId, const QString& nickname);
void createRoom(const QString& nickname, int maxPlayers);
void leaveRoom();
void setReady(bool ready);
void startGame();
void selectCard(const QString& cardUid);
void confirmAction(const QString& action, const QJsonObject& extra = {});
void effectResponse(const QJsonObject& response);
void listRooms();
void sendChat(const QString& text);
void sendBinaryFrame(const QByteArray& data);
signals:
void connected();
void disconnected();
void connectionError(const QString& error);
void roomStateUpdated(const QJsonObject& roomState);
void gameStarted(const QJsonObject& initConfig);
void gameStateSnapshot(const QJsonObject& snapshot);
void cardMoveEvent(const QJsonObject& event);
void cardFlipEvent(const QJsonObject& event);
void effectTriggered(const QJsonObject& event);
void requestChoice(const QJsonObject& request);
void playerExited(const QJsonObject& event);
void settlementNotify(const QJsonObject& event);
void gameEnded(const QJsonObject& result);
void errorNotify(const QJsonObject& error);
void chatMessage(const QJsonObject& msg);
void roomCreated(const QString& roomId);
void roomJoined(const QJsonObject& roomInfo);
void roomLeft();
void roomListReceived(const QJsonArray& rooms);
void settlementResult(const QJsonObject& data);
void binaryFrameReceived(const QByteArray& data);
private slots:
void onConnected();
void onDisconnected();
void onTextMessageReceived(const QString& message);
void onError(QAbstractSocket::SocketError error);
void onHeartbeat();
private:
explicit NetworkManager(QObject* parent = nullptr);
~NetworkManager() override = default;
void handleServerMessage(const QJsonObject& msg);
QWebSocket _socket;
QTimer _heartbeatTimer;
int _seqNum = 0;
QString _serverUrl;
};
#endif // NETWORKMANAGER_H
+113
View File
@@ -0,0 +1,113 @@
#include "CardInfoPopup.h"
#include "CardData.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QScreen>
CardInfoPopup::CardInfoPopup(QWidget* parent) : QWidget(parent)
{
setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
setAttribute(Qt::WA_ShowWithoutActivating);
setMinimumWidth(220);
setMaximumWidth(280);
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(14, 10, 14, 10);
lay->setSpacing(4);
_nameLabel = new QLabel(this);
_nameLabel->setStyleSheet("color: #ffd700; font-size: 15px; font-weight: bold; background:transparent;");
lay->addWidget(_nameLabel);
_mpLabel = new QLabel(this);
_mpLabel->setStyleSheet("color: #88bbff; font-size: 12px; background:transparent;");
lay->addWidget(_mpLabel);
_priorityLabel = new QLabel(this);
_priorityLabel->setStyleSheet("color: #aaa; font-size: 11px; background:transparent;");
lay->addWidget(_priorityLabel);
_winLabel = new QLabel(this);
_winLabel->setStyleSheet("color: #4ecdc4; font-size: 11px; background:transparent;");
_winLabel->setWordWrap(true);
lay->addWidget(_winLabel);
_effectLabel = new QLabel(this);
_effectLabel->setStyleSheet("color: #ccc; font-size: 11px; background:transparent;");
_effectLabel->setWordWrap(true);
lay->addWidget(_effectLabel);
_fadeAnim = new QPropertyAnimation(this, "popupOpacity", this);
_fadeAnim->setDuration(120);
connect(_fadeAnim, &QPropertyAnimation::finished, this, [this]() {
if (_opacity <= 0.01 && !_showRequested) hide();
});
_hideTimer = new QTimer(this);
_hideTimer->setSingleShot(true);
_hideTimer->setInterval(80);
connect(_hideTimer, &QTimer::timeout, this, &CardInfoPopup::doFadeOut);
}
void CardInfoPopup::showForCard(const QString& typeId, const QPoint& globalPos) {
const CardDef* def = CardDatabase::instance().getCardDef(typeId);
if (!def) { hidePopup(); return; }
_showRequested = true;
_hideTimer->stop();
_fadeAnim->stop();
_currentTypeId = typeId;
_nameLabel->setText(def->name);
_mpLabel->setText(QStringLiteral("人力值 (MP): %1").arg(def->mp));
_priorityLabel->setText(QStringLiteral("优先顺序: %1").arg(def->priority));
_winLabel->setText(QStringLiteral("胜利条件: %1").arg(def->winCondition));
_effectLabel->setText(def->effectDescription);
adjustSize();
QPoint pos = globalPos + QPoint(15, -height() / 2);
QScreen* screen = QApplication::screenAt(globalPos);
if (screen) {
QRect sr = screen->availableGeometry();
if (pos.x() + width() > sr.right()) pos.setX(globalPos.x() - width() - 15);
if (pos.y() < sr.top()) pos.setY(sr.top());
if (pos.y() + height() > sr.bottom()) pos.setY(sr.bottom() - height());
}
move(pos);
_opacity = 1.0;
show();
update();
_showRequested = false;
}
void CardInfoPopup::hidePopup() {
_currentTypeId.clear();
_hideTimer->start();
}
void CardInfoPopup::doFadeOut() {
if (!_currentTypeId.isEmpty()) return;
if (!isVisible()) return;
_fadeAnim->stop();
_fadeAnim->setStartValue(_opacity);
_fadeAnim->setEndValue(0.0);
_fadeAnim->start();
}
void CardInfoPopup::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.setOpacity(_opacity);
QPainterPath path;
path.addRoundedRect(rect().adjusted(1, 1, -1, -1), 10, 10);
p.fillPath(path, QColor(15, 15, 25, 235));
p.setPen(QPen(QColor(255, 215, 0, 60), 1));
p.drawPath(path);
}
+39
View File
@@ -0,0 +1,39 @@
#ifndef CARDINFOPOPUP_H
#define CARDINFOPOPUP_H
#include <QWidget>
#include <QPropertyAnimation>
#include <QTimer>
class QLabel;
class CardInfoPopup : public QWidget {
Q_OBJECT
Q_PROPERTY(qreal popupOpacity READ popupOpacity WRITE setPopupOpacity)
public:
explicit CardInfoPopup(QWidget* parent = nullptr);
void showForCard(const QString& typeId, const QPoint& globalPos);
void hidePopup();
qreal popupOpacity() const { return _opacity; }
void setPopupOpacity(qreal o) { _opacity = o; update(); }
protected:
void paintEvent(QPaintEvent* event) override;
private:
void doFadeOut();
QLabel* _nameLabel = nullptr;
QLabel* _mpLabel = nullptr;
QLabel* _priorityLabel = nullptr;
QLabel* _winLabel = nullptr;
QLabel* _effectLabel = nullptr;
QPropertyAnimation* _fadeAnim = nullptr;
QTimer* _hideTimer = nullptr;
qreal _opacity = 0.0;
QString _currentTypeId;
bool _showRequested = false;
};
#endif
+165
View File
@@ -0,0 +1,165 @@
#include "CardWidget.h"
#include "CardData.h"
#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QEnterEvent>
CardWidget::CardWidget(QWidget* parent) : QWidget(parent)
{
setFixedSize(normalSize());
setCursor(Qt::PointingHandCursor);
_backRaw = CardDatabase::instance().getCardBackImage();
rebuildCache();
}
void CardWidget::setCardType(const QString& typeId) {
_typeId = typeId;
loadPixmap();
rebuildCache();
update();
}
void CardWidget::setFaceUp(bool faceUp) { _faceUp = faceUp; update(); }
void CardWidget::setCardSelected(bool s) { _selected = s; update(); }
void CardWidget::setCardEnabled(bool e) {
_enabled = e;
setCursor(e ? Qt::PointingHandCursor : Qt::ArrowCursor);
update();
}
void CardWidget::setMini(bool m) {
_mini = m;
setFixedSize(m ? miniSize() : normalSize());
rebuildCache();
update();
}
void CardWidget::loadPixmap() {
if (!_typeId.isEmpty())
_faceRaw = CardDatabase::instance().getCardFrontImage(_typeId);
else
_faceRaw = QPixmap();
}
void CardWidget::rebuildCache() {
QSize target = size();
qreal dpr = devicePixelRatioF();
QSize pxSize(qRound(target.width() * dpr), qRound(target.height() * dpr));
if (!_faceRaw.isNull()) {
_faceScaled = _faceRaw.scaled(pxSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (_faceScaled.width() > pxSize.width() || _faceScaled.height() > pxSize.height()) {
int x = (_faceScaled.width() - pxSize.width()) / 2;
int y = (_faceScaled.height() - pxSize.height()) / 2;
_faceScaled = _faceScaled.copy(x, y, pxSize.width(), pxSize.height());
}
_faceScaled.setDevicePixelRatio(dpr);
} else {
_faceScaled = QPixmap();
}
if (!_backRaw.isNull()) {
_backScaled = _backRaw.scaled(pxSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (_backScaled.width() > pxSize.width() || _backScaled.height() > pxSize.height()) {
int x = (_backScaled.width() - pxSize.width()) / 2;
int y = (_backScaled.height() - pxSize.height()) / 2;
_backScaled = _backScaled.copy(x, y, pxSize.width(), pxSize.height());
}
_backScaled.setDevicePixelRatio(dpr);
} else {
_backScaled = QPixmap();
}
_cachedSize = target;
}
void CardWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.setRenderHint(QPainter::SmoothPixmapTransform);
QRectF cr = rect();
if (_selected) cr.adjust(0, 0, 0, -12);
int r = _mini ? 4 : 6;
if (_hovered && _enabled && !_mini) {
QRadialGradient glow(cr.center(), cr.width() * 0.8);
glow.setColorAt(0, QColor(120, 200, 230, 50));
glow.setColorAt(1, Qt::transparent);
p.setPen(Qt::NoPen);
p.setBrush(glow);
p.drawRoundedRect(cr.adjusted(-6, -6, 6, 6), r + 4, r + 4);
}
QPainterPath clip;
clip.addRoundedRect(cr, r, r);
p.setClipPath(clip);
QPixmap& px = (_faceUp && !_faceScaled.isNull()) ? _faceScaled : _backScaled;
if (!px.isNull()) {
p.drawPixmap(cr.topLeft(), px);
} else {
QLinearGradient bg(0, 0, 0, cr.height());
if (_faceUp) { bg.setColorAt(0, QColor("#d4c5a0")); bg.setColorAt(1, QColor("#b8a67a")); }
else { bg.setColorAt(0, QColor("#2a4a6b")); bg.setColorAt(1, QColor("#1a2f4a")); }
p.fillRect(cr, bg);
if (!_faceUp) {
p.setPen(QPen(QColor(255,255,255,20), 1));
for (int i = -200; i < 400; i += 12)
p.drawLine(QPointF(cr.left()+i, cr.top()), QPointF(cr.left()+i+100, cr.bottom()));
}
}
p.setClipping(false);
if (_faceUp && !_mini) {
const CardDef* def = CardDatabase::instance().getCardDef(_typeId);
if (def) {
p.setPen(Qt::NoPen);
p.setBrush(QColor(0,0,0,140));
QRectF badge(cr.left()+3, cr.top()+3, 22, 18);
p.drawRoundedRect(badge, 3, 3);
p.setFont(QFont("Microsoft YaHei", 9, QFont::Bold));
p.setPen(def->mp >= 0 ? QColor("#ffd700") : QColor("#ff4444"));
p.drawText(badge, Qt::AlignCenter, QString::number(def->mp));
}
}
QPen border;
if (_selected) border = QPen(QColor("#ffd700"), 2.5);
else if (_hovered && _enabled) border = QPen(QColor("#7ec8e3"), 2);
else border = QPen(QColor(0,0,0,60), 1);
p.setPen(border);
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(cr.adjusted(0.5,0.5,-0.5,-0.5), r, r);
if (!_enabled && !_mini) {
QPainterPath dimClip;
dimClip.addRoundedRect(cr, r, r);
p.setClipPath(dimClip);
p.fillRect(cr, QColor(0,0,0,90));
}
}
void CardWidget::enterEvent(QEnterEvent*) {
_hovered = true;
update();
if (_faceUp && !_typeId.isEmpty())
emit hovered(_typeId, true);
}
void CardWidget::leaveEvent(QEvent*) {
_hovered = false;
update();
emit hovered(_typeId, false);
}
void CardWidget::mousePressEvent(QMouseEvent* e) {
if (_enabled && e->button() == Qt::LeftButton) emit clicked(_uid);
if (_faceUp && e->button() == Qt::RightButton) emit rightClicked(_typeId);
}
void CardWidget::mouseDoubleClickEvent(QMouseEvent* e) {
if (_enabled && e->button() == Qt::LeftButton) emit doubleClicked(_uid);
}
+58
View File
@@ -0,0 +1,58 @@
#ifndef CARDWIDGET_H
#define CARDWIDGET_H
#include <QWidget>
#include <QPixmap>
class CardWidget : public QWidget {
Q_OBJECT
public:
explicit CardWidget(QWidget* parent = nullptr);
void setCardType(const QString& typeId);
void setFaceUp(bool faceUp);
void setCardSelected(bool selected);
void setCardEnabled(bool enabled);
void setMini(bool mini);
void rebuildCache();
QString cardUid() const { return _uid; }
void setCardUid(const QString& uid) { _uid = uid; }
QString cardTypeId() const { return _typeId; }
bool isFaceUp() const { return _faceUp; }
bool isCardSelected() const { return _selected; }
static QSize normalSize() { return {90, 130}; }
static QSize miniSize() { return {45, 65}; }
signals:
void clicked(const QString& uid);
void rightClicked(const QString& typeId);
void doubleClicked(const QString& uid);
void hovered(const QString& typeId, bool entered);
protected:
void paintEvent(QPaintEvent* event) override;
void enterEvent(QEnterEvent* event) override;
void leaveEvent(QEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
private:
void loadPixmap();
QString _uid;
QString _typeId;
bool _faceUp = false;
bool _selected = false;
bool _enabled = true;
bool _hovered = false;
bool _mini = false;
QPixmap _faceRaw;
QPixmap _backRaw;
QPixmap _faceScaled;
QPixmap _backScaled;
QSize _cachedSize;
};
#endif
+297
View File
@@ -0,0 +1,297 @@
#include "ChoiceOverlay.h"
#include "CardWidget.h"
#include "CardData.h"
#include "ElaPushButton.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QMouseEvent>
#include <QJsonArray>
#include <QScrollArea>
ChoiceOverlay::ChoiceOverlay(QWidget* parent) : QWidget(parent)
{
hide();
_panel = new QWidget(this);
_panel->setFixedWidth(660);
_panel->setStyleSheet("QWidget#choicePanel { background: rgba(12,12,22,245); border-radius: 16px; border: 1px solid rgba(255,215,0,50); }");
_panel->setObjectName("choicePanel");
auto* panelLayout = new QVBoxLayout(_panel);
panelLayout->setContentsMargins(24, 18, 24, 18);
panelLayout->setSpacing(12);
_titleLabel = new QLabel(_panel);
_titleLabel->setStyleSheet("color: #ffd700; font-size: 16px; font-weight: bold; background:transparent;");
_titleLabel->setWordWrap(true);
_titleLabel->setAlignment(Qt::AlignCenter);
panelLayout->addWidget(_titleLabel);
_contentLayout = new QVBoxLayout();
_contentLayout->setSpacing(10);
panelLayout->addLayout(_contentLayout);
}
void ChoiceOverlay::showChoice(const QString& choiceType, const QJsonObject& data) {
_choiceType = choiceType;
clearContent();
_titleLabel->setText(data["message"].toString());
if (choiceType == "view_cards")
buildViewCards(data);
else if (choiceType == "select_player")
buildSelectPlayer(data);
else if (choiceType == "select_card_from_hand" || choiceType == "all_select_card")
buildSelectCard(data);
else if (choiceType == "select_skill_card" || choiceType == "select_challenge_card" || choiceType == "select_harmony_card")
buildSelectZoneCard(data, choiceType);
else if (choiceType == "yes_no")
buildYesNo(data);
else if (choiceType == "select_option")
buildSelectOption(data);
_panel->setMaximumHeight(height() - 40);
_panel->adjustSize();
show(); raise();
_panel->move((width() - _panel->width()) / 2, qMax(20, (height() - _panel->height()) / 2));
}
void ChoiceOverlay::hideChoice() { clearContent(); hide(); }
void ChoiceOverlay::clearContent() {
for (auto* w : _dynamicWidgets) { _contentLayout->removeWidget(w); w->deleteLater(); }
_dynamicWidgets.clear();
}
void ChoiceOverlay::paintEvent(QPaintEvent*) {
QPainter p(this);
p.fillRect(rect(), QColor(0, 0, 0, 170));
}
void ChoiceOverlay::mousePressEvent(QMouseEvent* e) { e->accept(); }
QWidget* ChoiceOverlay::makeCardColumn(const QString& uid, const QString& typeId, const QString& name, bool faceUp, bool clickable) {
auto* col = new QWidget();
col->setStyleSheet("background:transparent;");
auto* vlay = new QVBoxLayout(col);
vlay->setContentsMargins(0, 0, 0, 0);
vlay->setSpacing(4);
vlay->setAlignment(Qt::AlignCenter);
auto* cw = new CardWidget(col);
cw->setCardUid(uid);
if (!typeId.isEmpty()) cw->setCardType(typeId);
cw->setFaceUp(faceUp);
cw->setCardEnabled(clickable);
vlay->addWidget(cw, 0, Qt::AlignCenter);
if (!name.isEmpty()) {
auto* lbl = new QLabel(name, col);
lbl->setStyleSheet("color:#ccc; font-size:11px; background:transparent;");
lbl->setAlignment(Qt::AlignCenter);
lbl->setFixedWidth(CardWidget::normalSize().width());
lbl->setWordWrap(true);
vlay->addWidget(lbl, 0, Qt::AlignCenter);
}
if (clickable) {
connect(cw, &CardWidget::clicked, this, [this, uid, typeId]() {
QJsonObject resp;
resp["card_uid"] = uid;
emit responded(resp);
hideChoice();
});
}
return col;
}
void ChoiceOverlay::buildViewCards(const QJsonObject& data) {
auto cards = data["cards"].toArray();
auto* scroll = new QScrollArea();
scroll->setWidgetResizable(true);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setMinimumHeight(180);
scroll->setMaximumHeight(350);
scroll->setStyleSheet("QScrollArea{background:transparent;border:none;}"
"QScrollBar{width:4px;height:4px;background:transparent;}"
"QScrollBar::handle{background:rgba(255,255,255,30);border-radius:2px;}");
auto* strip = new QWidget();
strip->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(strip);
hlay->setAlignment(Qt::AlignCenter);
hlay->setSpacing(12);
for (const auto& cv : cards) {
auto c = cv.toObject();
auto name = c["name"].toString();
if (name.isEmpty()) {
const CardDef* def = CardDatabase::instance().getCardDef(c["type_id"].toString());
if (def) name = def->name + QStringLiteral(" (MP:%1)").arg(def->mp);
} else {
name += QStringLiteral(" (MP:%1)").arg(c["mp"].toInt());
}
hlay->addWidget(makeCardColumn(c["uid"].toString(), c["type_id"].toString(), name, true, false));
}
scroll->setWidget(strip);
_contentLayout->addWidget(scroll);
_dynamicWidgets.append(scroll);
auto* btn = new ElaPushButton(QStringLiteral("确认"), _panel);
btn->setFixedSize(120, 36);
connect(btn, &ElaPushButton::clicked, this, [this]() {
QJsonObject r; r["ok"] = true; emit responded(r); hideChoice();
});
_contentLayout->addWidget(btn, 0, Qt::AlignCenter);
_dynamicWidgets.append(btn);
}
void ChoiceOverlay::buildSelectPlayer(const QJsonObject& data) {
auto targets = data["targets"].toArray();
auto* row = new QWidget();
row->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(row);
hlay->setAlignment(Qt::AlignCenter);
hlay->setSpacing(14);
for (const auto& tv : targets) {
auto t = tv.toObject();
auto pid = t["player_id"].toString();
auto nick = t["nickname"].toString();
auto* btn = new ElaPushButton(nick, row);
btn->setFixedSize(110, 40);
connect(btn, &ElaPushButton::clicked, this, [this, pid]() {
QJsonObject r; r["player_id"] = pid; emit responded(r); hideChoice();
});
hlay->addWidget(btn);
}
_contentLayout->addWidget(row);
_dynamicWidgets.append(row);
}
void ChoiceOverlay::buildSelectCard(const QJsonObject& data) {
auto cards = data["cards"].toArray();
auto* scroll = new QScrollArea();
scroll->setWidgetResizable(true);
scroll->setMinimumHeight(180);
scroll->setMaximumHeight(350);
scroll->setStyleSheet("QScrollArea{background:transparent;border:none;}"
"QScrollBar{width:4px;height:4px;background:transparent;}"
"QScrollBar::handle{background:rgba(255,255,255,30);border-radius:2px;}");
auto* strip = new QWidget();
strip->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(strip);
hlay->setAlignment(Qt::AlignCenter);
hlay->setSpacing(10);
for (const auto& cv : cards) {
auto c = cv.toObject();
const CardDef* def = CardDatabase::instance().getCardDef(c["type_id"].toString());
QString name = def ? def->name : "";
hlay->addWidget(makeCardColumn(c["uid"].toString(), c["type_id"].toString(), name, true, true));
}
scroll->setWidget(strip);
_contentLayout->addWidget(scroll);
_dynamicWidgets.append(scroll);
}
void ChoiceOverlay::buildYesNo(const QJsonObject&) {
auto* row = new QWidget();
row->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(row);
hlay->setAlignment(Qt::AlignCenter);
hlay->setSpacing(24);
auto* yBtn = new ElaPushButton(QStringLiteral(""), row);
yBtn->setFixedSize(100, 40);
connect(yBtn, &ElaPushButton::clicked, this, [this]() {
QJsonObject r; r["yes"] = true; emit responded(r); hideChoice();
});
auto* nBtn = new ElaPushButton(QStringLiteral(""), row);
nBtn->setFixedSize(100, 40);
connect(nBtn, &ElaPushButton::clicked, this, [this]() {
QJsonObject r; r["yes"] = false; emit responded(r); hideChoice();
});
hlay->addWidget(yBtn);
hlay->addWidget(nBtn);
_contentLayout->addWidget(row);
_dynamicWidgets.append(row);
}
void ChoiceOverlay::buildSelectOption(const QJsonObject& data) {
auto options = data["options"].toArray();
auto* row = new QWidget();
row->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(row);
hlay->setAlignment(Qt::AlignCenter);
hlay->setSpacing(14);
for (const auto& ov : options) {
auto o = ov.toObject();
auto val = o["value"].toString();
auto label = o["label"].toString();
auto* btn = new ElaPushButton(label, row);
btn->setFixedHeight(40);
btn->setMinimumWidth(100);
connect(btn, &ElaPushButton::clicked, this, [this, val]() {
QJsonObject r;
r["value"] = val;
emit responded(r);
hideChoice();
});
hlay->addWidget(btn);
}
_contentLayout->addWidget(row);
_dynamicWidgets.append(row);
}
void ChoiceOverlay::buildSelectZoneCard(const QJsonObject& data, const QString& type) {
auto cards = data["cards"].toArray();
auto* scroll = new QScrollArea();
scroll->setWidgetResizable(true);
scroll->setMinimumHeight(180);
scroll->setMaximumHeight(350);
scroll->setStyleSheet("QScrollArea{background:transparent;border:none;}"
"QScrollBar{width:4px;height:4px;background:transparent;}"
"QScrollBar::handle{background:rgba(255,255,255,30);border-radius:2px;}");
auto* strip = new QWidget();
strip->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(strip);
hlay->setAlignment(Qt::AlignCenter);
hlay->setSpacing(10);
for (const auto& cv : cards) {
auto c = cv.toObject();
auto uid = c["uid"].toString();
auto ownerId = c["owner_id"].toString();
auto typeId = c.contains("type_id") ? c["type_id"].toString() : QString();
bool faceUp = !typeId.isEmpty();
const CardDef* def = faceUp ? CardDatabase::instance().getCardDef(typeId) : nullptr;
QString name = def ? def->name : "";
auto* col = makeCardColumn(uid, typeId, name, faceUp, false);
auto* cw = col->findChild<CardWidget*>();
if (cw) {
cw->setCardEnabled(true);
disconnect(cw, nullptr, this, nullptr);
connect(cw, &CardWidget::clicked, this, [this, uid, ownerId, type]() {
QJsonObject r;
r["card_uid"] = uid;
if (type == "select_challenge_card") r["owner_id"] = ownerId;
emit responded(r);
hideChoice();
});
}
hlay->addWidget(col);
}
scroll->setWidget(strip);
_contentLayout->addWidget(scroll);
_dynamicWidgets.append(scroll);
}
+44
View File
@@ -0,0 +1,44 @@
#ifndef CHOICEOVERLAY_H
#define CHOICEOVERLAY_H
#include <QWidget>
#include <QJsonObject>
#include <QJsonArray>
class ElaPushButton;
class CardWidget;
class QVBoxLayout;
class QLabel;
class ChoiceOverlay : public QWidget {
Q_OBJECT
public:
explicit ChoiceOverlay(QWidget* parent = nullptr);
void showChoice(const QString& choiceType, const QJsonObject& choiceData);
void hideChoice();
signals:
void responded(const QJsonObject& response);
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
private:
void clearContent();
QWidget* makeCardColumn(const QString& uid, const QString& typeId, const QString& name, bool faceUp, bool clickable);
void buildViewCards(const QJsonObject& data);
void buildSelectPlayer(const QJsonObject& data);
void buildSelectCard(const QJsonObject& data);
void buildYesNo(const QJsonObject& data);
void buildSelectOption(const QJsonObject& data);
void buildSelectZoneCard(const QJsonObject& data, const QString& type);
QWidget* _panel = nullptr;
QVBoxLayout* _contentLayout = nullptr;
QLabel* _titleLabel = nullptr;
QVector<QWidget*> _dynamicWidgets;
QString _choiceType;
};
#endif
+755
View File
@@ -0,0 +1,755 @@
#include "GameWidget.h"
#include "SceneCardItem.h"
#include "CardWidget.h"
#include "PlayerSeatWidget.h"
#include "CardInfoPopup.h"
#include "ChoiceOverlay.h"
#include "SettlementOverlay.h"
#include "CardData.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaMessageBar.h"
#include "NetworkManager.h"
#include <QVBoxLayout>
#include "VoiceManager.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPainter>
#include <QDateTime>
#include <QTimer>
#include <QEvent>
#include <QKeyEvent>
#include <QPropertyAnimation>
#include "VoiceSettingsDialog.h"
#include <QGraphicsPixmapItem>
#include <QGraphicsTextItem>
#include <QOpenGLWidget>
static const QColor BG_MAIN("#1a1f2e");
static const QColor BG_PANEL("#252b3d");
static const QColor ACCENT("#5eb3e6");
static const QColor WARNING("#d94f5c");
static const QColor SUCCESS("#5cb85c");
static const QColor TEXT_PRI("#e0e0e0");
static const QColor TEXT_SEC("#8a8f9d");
static const QColor CARD_BORDER("#8b2635");
GameWidget::GameWidget(QWidget* parent) : QWidget(parent)
{
setStyleSheet(QStringLiteral("background:%1;").arg(BG_MAIN.name()));
setFocusPolicy(Qt::StrongFocus);
initScene();
initFloatingUI();
auto& net = NetworkManager::instance();
connect(&net, &NetworkManager::gameStarted, this, &GameWidget::onGameStart);
connect(&net, &NetworkManager::gameStateSnapshot, this, &GameWidget::onSnapshot);
connect(&net, &NetworkManager::settlementResult, this, &GameWidget::showSettlement);
connect(&net, &NetworkManager::gameEnded, this, [this](const QJsonObject& r) {
addLog(QStringLiteral("游戏结束: ") + r["message"].toString());
ElaMessageBar::information(ElaMessageBarType::Top, QStringLiteral("游戏结束"), r["message"].toString(), 5000, this);
_turnLabel->setText(QStringLiteral("游戏结束"));
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
});
connect(&net, &NetworkManager::errorNotify, this, [this](const QJsonObject& e) {
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("错误"), e["message"].toString(), 3000, this);
});
connect(&net, &NetworkManager::chatMessage, this, [this](const QJsonObject& m) {
auto nick = m["nickname"].toString();
auto text = m["text"].toString();
auto* item = new QListWidgetItem(nick + ": " + text);
item->setForeground(QColor("#e0e0e0"));
_chatList->addItem(item);
_chatList->scrollToBottom();
});
auto& voice = VoiceManager::instance();
connect(&voice, &VoiceManager::audioFrame, this, [](const QByteArray& pcm) {
NetworkManager::instance().sendBinaryFrame(pcm);
});
connect(&net, &NetworkManager::binaryFrameReceived, this, [](const QByteArray& data) {
VoiceManager::instance().processRemoteAudio(data);
});
connect(&voice, &VoiceManager::remoteSpeaking, this, [this](const QString& pid, bool speaking) {
if (_seats.contains(pid))
_seats[pid]->setIsCurrentTurn(speaking);
});
}
void GameWidget::initScene() {
_scene = new QGraphicsScene(this);
_view = new QGraphicsView(_scene, this);
_view->setRenderHint(QPainter::Antialiasing);
_view->setRenderHint(QPainter::SmoothPixmapTransform);
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setFrameShape(QFrame::NoFrame);
_view->setStyleSheet(QStringLiteral("background:%1;").arg(BG_MAIN.name()));
_view->setDragMode(QGraphicsView::NoDrag);
_view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_view);
QPixmap corpsePix(":/images/corpse");
if (!corpsePix.isNull())
_corpseItem = _scene->addPixmap(corpsePix.scaled(140, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation));
_harmonyLabel = _scene->addText("", QFont("Consolas", 12, QFont::Bold));
_harmonyLabel->setDefaultTextColor(TEXT_SEC);
}
void GameWidget::initFloatingUI() {
_topBar = new QWidget(this);
_topBar->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.6);border-bottom:1px solid rgba(94,179,230,0.2);"));
_topBar->setFixedHeight(44);
auto* tbLay = new QHBoxLayout(_topBar);
tbLay->setContentsMargins(16, 0, 16, 0);
_turnLabel = new QLabel(QStringLiteral("等待游戏开始"), _topBar);
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(ACCENT.name()));
_targetLabel = new QLabel(_topBar);
_targetLabel->setStyleSheet(QStringLiteral("color:%1;font-size:16px;font-weight:bold;font-family:Consolas;").arg(QColor("#d4c5a3").name()));
_targetLabel->setAlignment(Qt::AlignCenter);
tbLay->addWidget(_turnLabel);
tbLay->addStretch();
tbLay->addWidget(_targetLabel);
tbLay->addStretch();
auto* voiceStatus = new QLabel(QStringLiteral("🔇"), _topBar);
voiceStatus->setStyleSheet("color:#8a8f9d;font-size:16px;background:transparent;");
voiceStatus->setToolTip(QStringLiteral("V键: 按住说话 / 按下切换"));
tbLay->addWidget(voiceStatus);
auto* voiceSettingsBtn = new ElaPushButton(QStringLiteral(""), _topBar);
voiceSettingsBtn->setFixedSize(30, 30);
voiceSettingsBtn->setToolTip(QStringLiteral("语音设置"));
connect(voiceSettingsBtn, &ElaPushButton::clicked, this, [this]() {
VoiceSettingsDialog dlg(this);
dlg.exec();
});
tbLay->addWidget(voiceSettingsBtn);
connect(&VoiceManager::instance(), &VoiceManager::mutedChanged, voiceStatus, [voiceStatus](bool muted) {
voiceStatus->setText(muted ? QStringLiteral("🔇") : QStringLiteral("🎤"));
voiceStatus->setStyleSheet(muted ? "color:#8a8f9d;font-size:16px;background:transparent;" : "color:#5cb85c;font-size:16px;background:transparent;");
});
_actionBar = new QWidget(this);
_actionBar->setStyleSheet("background:transparent;");
_actionBar->setFixedHeight(50);
auto* abLay = new QHBoxLayout(_actionBar);
abLay->setContentsMargins(0, 4, 0, 4);
abLay->setSpacing(14);
abLay->addStretch();
_skillBtn = new ElaPushButton(QStringLiteral("特技"), _actionBar);
_skillBtn->setFixedSize(100, 38);
_skillBtn->setEnabled(false);
connect(_skillBtn, &ElaPushButton::clicked, this, &GameWidget::onSkill);
_harmonyBtn = new ElaPushButton(QStringLiteral("调和"), _actionBar);
_harmonyBtn->setFixedSize(100, 38);
_harmonyBtn->setEnabled(false);
connect(_harmonyBtn, &ElaPushButton::clicked, this, &GameWidget::onHarmony);
_challengeBtn = new ElaPushButton(QStringLiteral("质疑"), _actionBar);
_challengeBtn->setFixedSize(100, 38);
_challengeBtn->setEnabled(false);
connect(_challengeBtn, &ElaPushButton::clicked, this, &GameWidget::onChallenge);
abLay->addWidget(_skillBtn);
abLay->addWidget(_harmonyBtn);
abLay->addWidget(_challengeBtn);
abLay->addStretch();
_logPanel = new QWidget(this);
_logPanel->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.7);border-left:1px solid rgba(94,179,230,0.15);border-radius:0;"));
auto* logLay = new QVBoxLayout(_logPanel);
logLay->setContentsMargins(8, 8, 8, 8);
logLay->setSpacing(4);
auto* logT = new QLabel(QStringLiteral("游戏日志"), _logPanel);
logT->setStyleSheet(QStringLiteral("color:%1;font-size:12px;font-weight:bold;").arg(TEXT_SEC.name()));
logLay->addWidget(logT);
_logList = new QListWidget(_logPanel);
_logList->setStyleSheet(
QStringLiteral("QListWidget{background:transparent;border:none;color:%1;font-size:12px;font-family:'Consolas';}"
"QListWidget::item{padding:2px 0;}").arg(TEXT_SEC.name()));
logLay->addWidget(_logList, 1);
_cardInfoPopup = new CardInfoPopup(this);
_choiceOverlay = new ChoiceOverlay(this);
_choiceOverlay->hide();
connect(_choiceOverlay, &ChoiceOverlay::responded, this, &GameWidget::onEffectResponse);
_settlementOverlay = new SettlementOverlay(this);
_settlementOverlay->hide();
connect(_settlementOverlay, &SettlementOverlay::finished, this, [this]() { emit gameFinished(); });
_zoomOverlay = new QWidget(this);
_zoomOverlay->hide();
_zoomOverlay->setStyleSheet("background:rgba(0,0,0,180);");
_zoomCard = new CardWidget(_zoomOverlay);
_zoomCard->setFixedSize(270, 390);
_zoomCard->setCardEnabled(false);
_zoomOverlay->installEventFilter(this);
_playAnimWidget = new QWidget(this);
_playAnimWidget->hide();
_playAnimWidget->setStyleSheet("background:rgba(0,0,0,140);");
auto* paLay = new QVBoxLayout(_playAnimWidget);
paLay->setAlignment(Qt::AlignCenter);
paLay->setSpacing(10);
_playAnimCard = new CardWidget(_playAnimWidget);
_playAnimCard->setFixedSize(200, 290);
_playAnimCard->setCardEnabled(false);
paLay->addWidget(_playAnimCard, 0, Qt::AlignCenter);
_playAnimLabel = new QLabel(_playAnimWidget);
_playAnimLabel->setStyleSheet(QStringLiteral("color:%1;font-size:18px;font-weight:bold;background:transparent;").arg(QColor("#d4c5a3").name()));
_playAnimLabel->setAlignment(Qt::AlignCenter);
paLay->addWidget(_playAnimLabel, 0, Qt::AlignCenter);
_chatPanel = new QWidget(this);
_chatPanel->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.7);border-right:1px solid rgba(94,179,230,0.15);"));
auto* chatLay = new QVBoxLayout(_chatPanel);
chatLay->setContentsMargins(8, 8, 8, 8);
chatLay->setSpacing(4);
auto* chatTitle = new QLabel(QStringLiteral("聊天"), _chatPanel);
chatTitle->setStyleSheet(QStringLiteral("color:%1;font-size:12px;font-weight:bold;").arg(TEXT_SEC.name()));
chatLay->addWidget(chatTitle);
_chatList = new QListWidget(_chatPanel);
_chatList->setStyleSheet(QStringLiteral("QListWidget{background:transparent;border:none;color:%1;font-size:12px;}"
"QListWidget::item{padding:2px 0;}").arg(TEXT_PRI.name()));
_chatList->setWordWrap(true);
chatLay->addWidget(_chatList, 1);
auto* chatRow = new QHBoxLayout();
chatRow->setSpacing(4);
_chatInput = new ElaLineEdit(_chatPanel);
_chatInput->setPlaceholderText(QStringLiteral("输入消息..."));
_chatInput->setFixedHeight(28);
connect(_chatInput, &ElaLineEdit::returnPressed, this, &GameWidget::onSendChat);
auto* sendBtn = new ElaPushButton(QStringLiteral("发送"), _chatPanel);
sendBtn->setFixedSize(50, 28);
connect(sendBtn, &ElaPushButton::clicked, this, &GameWidget::onSendChat);
chatRow->addWidget(_chatInput, 1);
chatRow->addWidget(sendBtn);
chatLay->addLayout(chatRow);
_chatToggle = new ElaPushButton(QStringLiteral("💬"), this);
_chatToggle->setFixedSize(36, 36);
_chatToggle->setToolTip(QStringLiteral("聊天"));
connect(_chatToggle, &ElaPushButton::clicked, this, &GameWidget::toggleChat);
_chatPanel->hide();
}
void GameWidget::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
layoutFloatingUI();
layoutPlayerSeats();
QRect ga = gameArea();
int cx = ga.center().x();
_scene->setSceneRect(0, 0, width(), height());
if (_corpseItem) _corpseItem->setPos(cx - 70, ga.y() + ga.height() * 0.08);
if (_harmonyLabel) _harmonyLabel->setPos(cx - 80, ga.y() + ga.height() * 0.25);
qreal cardW = SceneCardItem::W;
int hCount = _harmonyCards.size();
if (hCount > 0) {
qreal hSpacing = qMin(cardW + 10, (ga.width() * 0.6) / qMax(1, hCount));
qreal hTotalW = (hCount - 1) * hSpacing + cardW;
qreal hStartX = cx - hTotalW / 2.0;
qreal hY = ga.y() + ga.height() * 0.32;
for (int i = 0; i < hCount; ++i)
_harmonyCards[i]->setPos(hStartX + i * hSpacing, hY);
}
int handCount = _handCards.size();
if (handCount > 0) {
qreal spacing = qMin(cardW + 10, (ga.width() * 0.8) / qMax(1, handCount));
qreal totalW = (handCount - 1) * spacing + cardW;
qreal startX = cx - totalW / 2.0;
qreal handY = ga.y() + ga.height() * 0.72;
for (int i = 0; i < handCount; ++i) {
_handCards[i]->setPos(startX + i * spacing, handY);
_handCards[i]->setZValue(10 + i);
}
}
}
void GameWidget::layoutFloatingUI() {
int w = width(), h = height();
int chatW = _chatVisible ? 200 : 0;
if (_chatVisible) {
_chatPanel->setGeometry(0, 44, 200, h - 44);
_chatPanel->show();
_chatPanel->raise();
} else {
_chatPanel->hide();
}
_chatToggle->move(chatW + 4, 48);
_chatToggle->raise();
_topBar->setGeometry(0, 0, w, 44);
_topBar->raise();
_actionBar->setGeometry(chatW, h - 54, w - chatW - 220, 50);
_actionBar->raise();
_logPanel->setGeometry(w - 220, 44, 220, h - 44);
_logPanel->raise();
_choiceOverlay->setGeometry(0, 0, w, h);
_settlementOverlay->setGeometry(0, 0, w, h);
_zoomOverlay->setGeometry(0, 0, w, h);
_playAnimWidget->setGeometry(0, 0, w, h);
_zoomCard->move((w - _zoomCard->width()) / 2, (h - _zoomCard->height()) / 2);
}
bool GameWidget::eventFilter(QObject* obj, QEvent* event) {
if (obj == _zoomOverlay && event->type() == QEvent::MouseButtonPress) {
_zoomOverlay->hide();
return true;
}
return QWidget::eventFilter(obj, event);
}
// ---- Game lifecycle ----
void GameWidget::onGameStart(const QJsonObject& config) {
resetGame();
_chatList->clear();
_localPlayerId = config["your_player_id"].toString();
_harmonyTarget = config["harmony_target"].toInt();
_targetLabel->setText(QStringLiteral("目标值: %1").arg(_harmonyTarget));
_turnLabel->setText(QStringLiteral("游戏开始!"));
_harmonyLabel->setPlainText(QStringLiteral("调和区 (目标: %1)").arg(_harmonyTarget));
addLog(QStringLiteral("游戏开始 - 调和目标值 %1").arg(_harmonyTarget));
}
void GameWidget::onSnapshot(const QJsonObject& snap) {
_localPlayerId = snap["your_player_id"].toString();
auto phase = snap["phase"].toString();
auto turnPhase = snap["turn_phase"].toString();
auto turnPlayer = snap["current_turn"].toString();
auto players = snap["players"].toArray();
_playerOrder.clear();
for (const auto& pv : players)
_playerOrder.append(pv.toObject()["player_id"].toString());
for (const auto& pv : players) {
auto po = pv.toObject();
auto pid = po["player_id"].toString();
if (pid == _localPlayerId) continue;
if (!_seats.contains(pid)) {
auto* seat = new PlayerSeatWidget(this);
connect(seat, &PlayerSeatWidget::playerClicked, this, &GameWidget::onPlayerSeatClicked);
_seats[pid] = seat;
}
_seats[pid]->setPlayerData(po);
_seats[pid]->setIsCurrentTurn(pid == turnPlayer);
}
QStringList gone;
for (auto it = _seats.begin(); it != _seats.end(); ++it) {
bool found = false;
for (const auto& pv : players)
if (pv.toObject()["player_id"].toString() == it.key() && it.key() != _localPlayerId)
found = true;
if (!found) gone.append(it.key());
}
for (const auto& k : gone) { _seats[k]->deleteLater(); _seats.remove(k); }
layoutPlayerSeats();
updateHandCards(snap["hand_cards"].toArray());
updateHarmonyZone(snap["harmony_zone"].toArray());
if (snap.contains("effect")) {
auto eInfo = snap["effect"].toObject();
if (eInfo["needs_response"].toBool()) {
auto ct = eInfo["choice_type"].toString();
if (ct == "all_select_card" || ct == "select_card_from_hand") {
auto cd = eInfo["choice_data"].toObject();
if (!cd.contains("cards")) { cd["cards"] = snap["hand_cards"]; eInfo["choice_data"] = cd; }
}
}
handleEffectState(eInfo);
return;
}
_choiceOverlay->hideChoice();
bool myTurn = (turnPlayer == _localPlayerId);
if (phase == "all_exited") {
_turnLabel->setText(QStringLiteral("所有人已退出 — 等待结算"));
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
return;
}
if (!myTurn) {
QString nick;
for (const auto& pv : players)
if (pv.toObject()["player_id"].toString() == turnPlayer)
nick = pv.toObject()["nickname"].toString();
_turnLabel->setText(QStringLiteral("等待 %1 行动...").arg(nick));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;").arg(TEXT_SEC.name()));
_phase = Phase::Idle;
setActionsEnabled(false, false, false);
return;
}
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(ACCENT.name()));
if (turnPhase == "select_card") {
_turnLabel->setText(QStringLiteral("你的回合 — 选择手牌"));
_phase = Phase::SelectCard;
for (auto* c : _handCards) {
const CardDef* def = CardDatabase::instance().getCardDef(c->typeId());
c->setCardEnabled(!(def && def->unusable));
}
setActionsEnabled(false, false, false);
} else if (turnPhase == "select_action") {
_turnLabel->setText(QStringLiteral("选择行动方式"));
_phase = Phase::SelectAction;
} else if (turnPhase == "select_target") {
_turnLabel->setText(QStringLiteral("选择质疑目标"));
_phase = Phase::SelectTarget;
}
for (const auto& pv : players) {
if (pv.toObject()["player_id"].toString() == _localPlayerId && pv.toObject()["is_exited"].toBool()) {
_turnLabel->setText(QStringLiteral("你已暂时退出"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;").arg(TEXT_SEC.name()));
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
}
}
}
void GameWidget::resetGame() {
VoiceManager::instance().stop();
for (auto* c : _handCards) { _scene->removeItem(c); delete c; }
_handCards.clear();
for (auto* c : _harmonyCards) { _scene->removeItem(c); delete c; }
_harmonyCards.clear();
for (auto* s : _seats) s->deleteLater();
_seats.clear();
_playerOrder.clear();
_selectedCardUid.clear();
_phase = Phase::Idle;
_logList->clear();
setActionsEnabled(false, false, false);
_turnLabel->setText(QStringLiteral("等待游戏开始"));
_targetLabel->clear();
_harmonyLabel->setPlainText("");
_choiceOverlay->hideChoice();
}
// ---- Scene layout ----
void GameWidget::layoutPlayerSeats() {
QVector<QString> others;
for (const auto& pid : _playerOrder)
if (pid != _localPlayerId && _seats.contains(pid))
others.append(pid);
int n = others.size();
if (n == 0) return;
QRect ga = gameArea();
int gx = ga.x(), gy = ga.y(), gw = ga.width(), gh = ga.height();
int pw = 180, ph = 130;
struct Pos { int x, y; };
QVector<Pos> positions;
if (n == 1) {
positions = {{gx + gw / 2 - pw / 2, gy}};
} else if (n == 2) {
positions = {{gx + gw / 4 - pw / 2, gy}, {gx + 3 * gw / 4 - pw / 2, gy}};
} else if (n == 3) {
positions = {{gx, gy + gh / 2 - ph / 2}, {gx + gw / 2 - pw / 2, gy}, {gx + gw - pw, gy + gh / 2 - ph / 2}};
} else if (n == 4) {
positions = {{gx, gy + gh / 2 - ph / 2}, {gx + gw / 3 - pw / 2, gy}, {gx + 2 * gw / 3 - pw / 2, gy}, {gx + gw - pw, gy + gh / 2 - ph / 2}};
} else {
positions.append({gx, gy + gh / 2 - ph / 2});
int topCount = n - 2;
for (int i = 0; i < topCount; ++i)
positions.append({gx + int((i + 1) * double(gw) / (topCount + 1) - pw / 2), gy});
positions.append({gx + gw - pw, gy + gh / 2 - ph / 2});
}
for (int i = 0; i < n && i < positions.size(); ++i) {
_seats[others[i]]->setGeometry(positions[i].x, positions[i].y, pw, ph);
_seats[others[i]]->show();
_seats[others[i]]->raise();
}
}
void GameWidget::updateHandCards(const QJsonArray& cards) {
for (auto* c : _handCards) { _scene->removeItem(c); delete c; }
_handCards.clear();
_selectedCardUid.clear();
setActionsEnabled(false, false, false);
QRect ga = gameArea();
int cx = ga.center().x();
int count = cards.size();
qreal cardW = SceneCardItem::W;
qreal spacing = qMin(cardW + 10, (ga.width() * 0.8) / qMax(1, count));
qreal totalW = (count - 1) * spacing + cardW;
qreal startX = cx - totalW / 2.0;
qreal handY = ga.y() + ga.height() * 0.72;
for (int i = 0; i < count; ++i) {
auto c = cards[i].toObject();
auto* item = new SceneCardItem();
item->setCardData(c["uid"].toString(), c["type_id"].toString(), true);
item->setPos(startX + i * spacing, handY);
item->setZValue(10 + i);
const CardDef* def = CardDatabase::instance().getCardDef(c["type_id"].toString());
item->setCardEnabled(_phase == Phase::SelectCard && !(def && def->unusable));
connect(item, &SceneCardItem::clicked, this, &GameWidget::onSceneCardClicked);
connect(item, &SceneCardItem::hoverIn, this, &GameWidget::onSceneCardHover);
connect(item, &SceneCardItem::hoverOut, this, &GameWidget::onSceneCardHoverOut);
connect(item, &SceneCardItem::rightClicked, this, &GameWidget::showCardZoom);
_scene->addItem(item);
_handCards.append(item);
}
}
void GameWidget::updateHarmonyZone(const QJsonArray& cards) {
for (auto* c : _harmonyCards) { _scene->removeItem(c); delete c; }
_harmonyCards.clear();
_harmonyLabel->setPlainText(QStringLiteral("调和区 (%1张 / 目标: %2)").arg(cards.size()).arg(_harmonyTarget));
QRect ga = gameArea();
int cx = ga.center().x();
int count = cards.size();
qreal cardW = SceneCardItem::W;
qreal spacing = qMin(cardW + 10, (ga.width() * 0.6) / qMax(1, count));
qreal totalW = (count - 1) * spacing + cardW;
qreal startX = cx - totalW / 2.0;
qreal hY = ga.y() + ga.height() * 0.32;
for (int i = 0; i < count; ++i) {
auto c = cards[i].toObject();
auto* item = new SceneCardItem();
item->setCardData(c["uid"].toString(), c.contains("type_id") ? c["type_id"].toString() : "", c["face_up"].toBool(false));
item->setPos(startX + i * spacing, hY);
item->setScale(0.8);
item->setCardEnabled(false);
item->setZValue(5);
_scene->addItem(item);
_harmonyCards.append(item);
}
}
// ---- Interaction handlers ----
void GameWidget::onSceneCardClicked(const QString& uid) {
if (_phase != Phase::SelectCard) return;
bool wasSelected = false;
for (auto* c : _handCards) {
if (c->uid() == uid) {
wasSelected = c->isCardSelected();
c->setCardSelected(!wasSelected);
} else {
c->setCardSelected(false);
}
}
if (wasSelected) { _selectedCardUid.clear(); setActionsEnabled(false, false, false); }
else { _selectedCardUid = uid; setActionsEnabled(true, true, true); }
}
void GameWidget::onSceneCardHover(const QString& typeId) {
auto* item = qobject_cast<SceneCardItem*>(sender());
if (item) {
QPointF scenePos = item->mapToScene(SceneCardItem::W, 0);
QPoint viewPos = _view->mapFromScene(scenePos);
QPoint globalPos = _view->viewport()->mapToGlobal(viewPos);
_cardInfoPopup->showForCard(typeId, globalPos);
}
}
void GameWidget::onSceneCardHoverOut() {
_cardInfoPopup->hidePopup();
}
void GameWidget::onSkill() {
if (_selectedCardUid.isEmpty()) return;
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
const CardDef* def = CardDatabase::instance().getCardDef(typeId);
showPlayAnimation(typeId, QStringLiteral("特技: ") + (def ? def->name : typeId));
NetworkManager::instance().selectCard(_selectedCardUid);
NetworkManager::instance().confirmAction("skill");
_phase = Phase::WaitResponse;
setActionsEnabled(false, false, false);
addLog(QStringLiteral("使用特技: ") + (def ? def->name : typeId));
}
void GameWidget::onHarmony() {
if (_selectedCardUid.isEmpty()) return;
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
showPlayAnimation(typeId, QStringLiteral("调和"));
NetworkManager::instance().selectCard(_selectedCardUid);
NetworkManager::instance().confirmAction("harmony");
_phase = Phase::WaitResponse;
setActionsEnabled(false, false, false);
addLog(QStringLiteral("放入调和区"));
}
void GameWidget::onChallenge() {
if (_selectedCardUid.isEmpty()) return;
_phase = Phase::SelectTarget;
_turnLabel->setText(QStringLiteral("点击目标玩家"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(WARNING.name()));
setActionsEnabled(false, false, false);
for (auto* s : _seats) s->setCursor(Qt::PointingHandCursor);
addLog(QStringLiteral("选择质疑目标..."));
}
void GameWidget::onPlayerSeatClicked(const QString& playerId) {
if (_phase != Phase::SelectTarget || _selectedCardUid.isEmpty()) return;
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
showPlayAnimation(typeId, QStringLiteral("质疑"));
NetworkManager::instance().selectCard(_selectedCardUid);
QJsonObject extra;
extra["target_player_id"] = playerId;
NetworkManager::instance().confirmAction("challenge", extra);
_phase = Phase::WaitResponse;
for (auto* s : _seats) s->setCursor(Qt::ArrowCursor);
addLog(QStringLiteral("质疑 ") + playerId);
}
// ---- Effect / Settlement / Overlays ----
void GameWidget::handleEffectState(const QJsonObject& info) {
if (info["needs_response"].toBool()) {
auto ct = info["choice_type"].toString();
auto cd = info["choice_data"].toObject();
addLog(QStringLiteral("效果: ") + info["card_type"].toString() + " - " + cd["message"].toString());
_choiceOverlay->showChoice(ct, cd);
_turnLabel->setText(QStringLiteral("效果结算 — 请做出选择"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(QColor("#d4c5a3").name()));
} else {
_turnLabel->setText(info["waiting_message"].toString());
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;").arg(TEXT_SEC.name()));
addLog(QStringLiteral("效果结算中: ") + info["card_type"].toString());
}
}
void GameWidget::onEffectResponse(const QJsonObject& resp) {
NetworkManager::instance().effectResponse(resp);
_turnLabel->setText(QStringLiteral("等待效果结算..."));
}
void GameWidget::showSettlement(const QJsonObject& data) {
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
_turnLabel->setText(QStringLiteral("结算中..."));
addLog(QStringLiteral("进入结算阶段"));
_settlementOverlay->showSettlement(data);
}
void GameWidget::showCardZoom(const QString& typeId) {
if (typeId.isEmpty()) return;
_zoomCard->setCardType(typeId);
_zoomCard->setFaceUp(true);
_zoomCard->rebuildCache();
_zoomOverlay->setGeometry(rect());
_zoomCard->move((width() - _zoomCard->width()) / 2, (height() - _zoomCard->height()) / 2);
_zoomOverlay->show();
_zoomOverlay->raise();
}
void GameWidget::showPlayAnimation(const QString& typeId, const QString& actionName) {
_playAnimCard->setCardType(typeId);
_playAnimCard->setFaceUp(true);
_playAnimCard->rebuildCache();
_playAnimLabel->setText(actionName);
_playAnimWidget->setGeometry(rect());
_playAnimWidget->show();
_playAnimWidget->raise();
QTimer::singleShot(1200, this, [this]() { _playAnimWidget->hide(); });
}
void GameWidget::animateCardToPos(SceneCardItem* card, const QPointF& target, int duration) {
auto* anim = new QPropertyAnimation(card, "pos", this);
anim->setDuration(duration);
anim->setStartValue(card->pos());
anim->setEndValue(target);
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
// ---- Helpers ----
void GameWidget::setActionsEnabled(bool s, bool h, bool c) {
_skillBtn->setEnabled(s);
_harmonyBtn->setEnabled(h);
_challengeBtn->setEnabled(c);
}
void GameWidget::addLog(const QString& text) {
auto* item = new QListWidgetItem("[" + QDateTime::currentDateTime().toString("hh:mm:ss") + "] " + text);
item->setForeground(text.contains(QStringLiteral("效果")) ? ACCENT : TEXT_SEC);
_logList->addItem(item);
_logList->scrollToBottom();
}
QRect GameWidget::gameArea() const {
int chatW = _chatVisible ? 200 : 0;
return {chatW, 44, width() - chatW - 220, height() - 44 - 54};
}
void GameWidget::onSendChat() {
auto text = _chatInput->text().trimmed();
if (text.isEmpty()) return;
NetworkManager::instance().sendChat(text);
_chatInput->clear();
}
void GameWidget::toggleChat() {
_chatVisible = !_chatVisible;
layoutFloatingUI();
layoutPlayerSeats();
QRect ga = gameArea();
int cx = ga.center().x();
_scene->setSceneRect(0, 0, width(), height());
if (_corpseItem) _corpseItem->setPos(cx - 70, ga.y() + ga.height() * 0.08);
if (_harmonyLabel) _harmonyLabel->setPos(cx - 80, ga.y() + ga.height() * 0.25);
qreal cardW = SceneCardItem::W;
int hc = _harmonyCards.size();
if (hc > 0) {
qreal sp = qMin(cardW + 10, (ga.width() * 0.6) / qMax(1, hc));
qreal tw = (hc - 1) * sp + cardW;
for (int i = 0; i < hc; ++i) _harmonyCards[i]->setPos(cx - tw / 2.0 + i * sp, ga.y() + ga.height() * 0.32);
}
int nc = _handCards.size();
if (nc > 0) {
qreal sp = qMin(cardW + 10, (ga.width() * 0.8) / qMax(1, nc));
qreal tw = (nc - 1) * sp + cardW;
for (int i = 0; i < nc; ++i) { _handCards[i]->setPos(cx - tw / 2.0 + i * sp, ga.y() + ga.height() * 0.72); _handCards[i]->setZValue(10 + i); }
}
}
void GameWidget::keyPressEvent(QKeyEvent* e) {
if (e->key() == Qt::Key_V && !e->isAutoRepeat()) {
if (_chatInput && _chatInput->hasFocus()) { QWidget::keyPressEvent(e); return; }
VoiceManager::instance().keyDown();
return;
}
QWidget::keyPressEvent(e);
}
void GameWidget::keyReleaseEvent(QKeyEvent* e) {
if (e->key() == Qt::Key_V && !e->isAutoRepeat()) {
if (_chatInput && _chatInput->hasFocus()) { QWidget::keyReleaseEvent(e); return; }
VoiceManager::instance().keyUp();
return;
}
QWidget::keyReleaseEvent(e);
}
+113
View File
@@ -0,0 +1,113 @@
#ifndef GAMEWIDGET_H
#define GAMEWIDGET_H
#include <QWidget>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QJsonObject>
#include <QJsonArray>
#include <QMap>
class SceneCardItem;
class CardWidget;
class CardInfoPopup;
class ChoiceOverlay;
class SettlementOverlay;
class PlayerSeatWidget;
class ElaPushButton;
class QLabel;
class QListWidget;
class QGraphicsPixmapItem;
class QGraphicsTextItem;
class ElaLineEdit;
class GameWidget : public QWidget {
Q_OBJECT
public:
explicit GameWidget(QWidget* parent = nullptr);
void onGameStart(const QJsonObject& config);
void onSnapshot(const QJsonObject& snapshot);
void resetGame();
signals:
void gameFinished();
protected:
void resizeEvent(QResizeEvent* event) override;
bool eventFilter(QObject* obj, QEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
private:
void initScene();
void initFloatingUI();
void layoutFloatingUI();
void layoutPlayerSeats();
void updateHandCards(const QJsonArray& cards);
void updateHarmonyZone(const QJsonArray& cards);
void addLog(const QString& text);
void setActionsEnabled(bool s, bool h, bool c);
void onSceneCardClicked(const QString& uid);
void onSceneCardHover(const QString& typeId);
void onSceneCardHoverOut();
void onSkill();
void onHarmony();
void onChallenge();
void onPlayerSeatClicked(const QString& playerId);
void onEffectResponse(const QJsonObject& resp);
void handleEffectState(const QJsonObject& info);
void showSettlement(const QJsonObject& data);
void showCardZoom(const QString& typeId);
void showPlayAnimation(const QString& typeId, const QString& actionName);
void animateCardToPos(SceneCardItem* card, const QPointF& target, int duration = 400);
void onSendChat();
void toggleChat();
QRect gameArea() const;
enum class Phase { Idle, SelectCard, SelectAction, SelectTarget, WaitResponse, Watching };
Phase _phase = Phase::Idle;
QString _localPlayerId;
QString _selectedCardUid;
int _harmonyTarget = 0;
QGraphicsScene* _scene = nullptr;
QGraphicsView* _view = nullptr;
QGraphicsPixmapItem* _corpseItem = nullptr;
QGraphicsTextItem* _harmonyLabel = nullptr;
QVector<SceneCardItem*> _handCards;
QVector<SceneCardItem*> _harmonyCards;
QMap<QString, PlayerSeatWidget*> _seats;
QVector<QString> _playerOrder;
QWidget* _topBar = nullptr;
QLabel* _turnLabel = nullptr;
QLabel* _targetLabel = nullptr;
QWidget* _actionBar = nullptr;
ElaPushButton* _skillBtn = nullptr;
ElaPushButton* _harmonyBtn = nullptr;
ElaPushButton* _challengeBtn = nullptr;
QWidget* _logPanel = nullptr;
QListWidget* _logList = nullptr;
QWidget* _chatPanel = nullptr;
QListWidget* _chatList = nullptr;
ElaLineEdit* _chatInput = nullptr;
ElaPushButton* _chatToggle = nullptr;
bool _chatVisible = false;
CardInfoPopup* _cardInfoPopup = nullptr;
ChoiceOverlay* _choiceOverlay = nullptr;
SettlementOverlay* _settlementOverlay = nullptr;
QWidget* _zoomOverlay = nullptr;
CardWidget* _zoomCard = nullptr;
QWidget* _playAnimWidget = nullptr;
CardWidget* _playAnimCard = nullptr;
QLabel* _playAnimLabel = nullptr;
};
#endif
+297
View File
@@ -0,0 +1,297 @@
#include "LobbyWidget.h"
#include "CardWidget.h"
#include "CardData.h"
#include "ElaLineEdit.h"
#include "ElaPushButton.h"
#include "ElaSpinBox.h"
#include "ElaMessageBar.h"
#include "NetworkManager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QLinearGradient>
#include <QFrame>
#include <QScrollArea>
#include <QScroller>
LobbyWidget::LobbyWidget(QWidget* parent) : QWidget(parent)
{
initUI();
auto& net = NetworkManager::instance();
connect(&net, &NetworkManager::connected, this, [this]() {
_statusLabel->setText(QStringLiteral("● 已连接到服务器"));
_statusLabel->setStyleSheet("color: #4ecdc4; font-size: 13px; background:transparent;");
_connectBtn->setText(QStringLiteral("断开"));
_connectBtn->setEnabled(true);
_createBtn->setEnabled(true);
_joinBtn->setEnabled(true);
ElaMessageBar::success(ElaMessageBarType::TopRight, QStringLiteral("连接成功"), QStringLiteral("已连接,可以创建或加入房间"), 2000, this);
});
connect(&net, &NetworkManager::disconnected, this, [this]() {
_statusLabel->setText(QStringLiteral("○ 未连接"));
_statusLabel->setStyleSheet("color: #888; font-size: 13px; background:transparent;");
_connectBtn->setText(QStringLiteral("连接"));
_connectBtn->setEnabled(true);
_createBtn->setEnabled(false);
_joinBtn->setEnabled(false);
});
connect(&net, &NetworkManager::connectionError, this, [this](const QString& err) {
_connectBtn->setEnabled(true);
_connectBtn->setText(QStringLiteral("连接"));
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("连接失败"), err, 3000, this);
});
connect(&net, &NetworkManager::roomCreated, this, [this](const QString& roomId) {
ElaMessageBar::success(ElaMessageBarType::Top, QStringLiteral("房间已创建"), QStringLiteral("房间号: ") + roomId, 3000, this);
emit createdRoom();
});
connect(&net, &NetworkManager::roomJoined, this, [this](const QJsonObject& info) {
auto rid = info["room_id"].toString();
ElaMessageBar::success(ElaMessageBarType::Top, QStringLiteral("已加入房间"), QStringLiteral("房间号: ") + rid + QStringLiteral(" 正在进入等待室..."), 2000, this);
emit joinedRoom();
});
connect(&net, &NetworkManager::errorNotify, this, [this](const QJsonObject& err) {
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("错误"), err["message"].toString(), 3000, this);
});
}
void LobbyWidget::initUI() {
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
auto* topSection = new QWidget(this);
auto* topLayout = new QVBoxLayout(topSection);
topLayout->setAlignment(Qt::AlignCenter);
topLayout->setContentsMargins(0, 20, 0, 10);
auto* card = new QWidget(topSection);
card->setFixedSize(440, 500);
card->setStyleSheet("QWidget#lobbyCard { background: rgba(37,43,61,220); border-radius: 16px; border: 1px solid rgba(94,179,230,30); }");
card->setObjectName("lobbyCard");
auto* layout = new QVBoxLayout(card);
layout->setContentsMargins(30, 20, 30, 18);
layout->setSpacing(10);
auto* titleIcon = new QLabel(card);
titleIcon->setPixmap(QPixmap(":/images/corpse").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
titleIcon->setAlignment(Qt::AlignCenter);
titleIcon->setStyleSheet("background:transparent;");
layout->addWidget(titleIcon);
auto* title = new QLabel(QStringLiteral("冰冷的她醒来之前"), card);
title->setStyleSheet("color: white; font-size: 20px; font-weight: bold; background:transparent;");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
layout->addSpacing(6);
auto makeRow = [&](const QString& labelText, QWidget* input) {
auto* row = new QHBoxLayout();
auto* lbl = new QLabel(labelText, card);
lbl->setStyleSheet("color:#bbb; font-size:13px; background:transparent; min-width:55px;");
row->addWidget(lbl);
row->addWidget(input, 1);
layout->addLayout(row);
};
_serverEdit = new ElaLineEdit(card);
_serverEdit->setText("ws://127.0.0.1:8080/ws");
_serverEdit->setFixedHeight(30);
makeRow(QStringLiteral("服务器"), _serverEdit);
_nicknameEdit = new ElaLineEdit(card);
_nicknameEdit->setText(QStringLiteral("玩家"));
_nicknameEdit->setFixedHeight(30);
makeRow(QStringLiteral("昵称"), _nicknameEdit);
auto* connRow = new QHBoxLayout();
_statusLabel = new QLabel(QStringLiteral("○ 未连接 (请先启动服务端)"), card);
_statusLabel->setStyleSheet("color: #888; font-size: 12px; background:transparent;");
_connectBtn = new ElaPushButton(QStringLiteral("连接"), card);
_connectBtn->setFixedSize(80, 30);
connect(_connectBtn, &ElaPushButton::clicked, this, &LobbyWidget::onConnect);
connRow->addWidget(_statusLabel);
connRow->addStretch();
connRow->addWidget(_connectBtn);
layout->addLayout(connRow);
auto* sep1 = new QFrame(card);
sep1->setFrameShape(QFrame::HLine);
sep1->setStyleSheet("color:rgba(255,255,255,15); background:transparent;");
layout->addWidget(sep1);
auto* createRow = new QHBoxLayout();
auto* maxLbl = new QLabel(QStringLiteral("人数"), card);
maxLbl->setStyleSheet("color:#bbb; font-size:13px; background:transparent;");
_maxPlayersSpin = new ElaSpinBox(card);
_maxPlayersSpin->setRange(3, 6);
_maxPlayersSpin->setValue(4);
_maxPlayersSpin->setFixedWidth(70);
_createBtn = new ElaPushButton(QStringLiteral("创建房间"), card);
_createBtn->setFixedHeight(32);
_createBtn->setEnabled(false);
connect(_createBtn, &ElaPushButton::clicked, this, &LobbyWidget::onCreateRoom);
createRow->addWidget(maxLbl);
createRow->addWidget(_maxPlayersSpin);
createRow->addStretch();
createRow->addWidget(_createBtn);
layout->addLayout(createRow);
auto* joinRow = new QHBoxLayout();
_roomIdEdit = new ElaLineEdit(card);
_roomIdEdit->setPlaceholderText(QStringLiteral("输入房间号"));
_roomIdEdit->setFixedHeight(30);
_joinBtn = new ElaPushButton(QStringLiteral("加入"), card);
_joinBtn->setFixedSize(80, 32);
_joinBtn->setEnabled(false);
connect(_joinBtn, &ElaPushButton::clicked, this, &LobbyWidget::onJoinRoom);
joinRow->addWidget(_roomIdEdit, 1);
joinRow->addWidget(_joinBtn);
layout->addLayout(joinRow);
auto* sep2 = new QFrame(card);
sep2->setFrameShape(QFrame::HLine);
sep2->setStyleSheet("color:rgba(255,255,255,15); background:transparent;");
layout->addWidget(sep2);
auto* testTitle = new QLabel(QStringLiteral("快速测试(含人机占位,连接后可用)"), card);
testTitle->setStyleSheet("color:#777; font-size:11px; background:transparent;");
testTitle->setAlignment(Qt::AlignCenter);
layout->addWidget(testTitle);
auto* testRow = new QHBoxLayout();
testRow->setSpacing(8);
for (int n = 3; n <= 6; ++n) {
auto* btn = new ElaPushButton(QStringLiteral("%1人局").arg(n), card);
btn->setFixedHeight(30);
btn->setEnabled(false);
connect(btn, &ElaPushButton::clicked, this, [this, n]() { onJoinTestRoom(n); });
connect(&NetworkManager::instance(), &NetworkManager::connected, btn, [btn]() { btn->setEnabled(true); });
connect(&NetworkManager::instance(), &NetworkManager::disconnected, btn, [btn]() { btn->setEnabled(false); });
testRow->addWidget(btn);
}
layout->addLayout(testRow);
topLayout->addWidget(card);
root->addWidget(topSection, 1);
buildCardGallery(this, root);
}
void LobbyWidget::buildCardGallery(QWidget* parent, QLayout* parentLayout) {
auto* section = new QWidget(parent);
section->setFixedHeight(180);
section->setStyleSheet("background: rgba(0,0,0,80); border-top: 1px solid rgba(255,255,255,10);");
auto* sLayout = new QVBoxLayout(section);
sLayout->setContentsMargins(20, 8, 20, 8);
sLayout->setSpacing(4);
auto* label = new QLabel(QStringLiteral("卡牌一览"), section);
label->setStyleSheet("color: #999; font-size: 13px; font-weight: bold; background:transparent;");
sLayout->addWidget(label);
auto* scroll = new QScrollArea(section);
scroll->setWidgetResizable(true);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setStyleSheet(
"QScrollArea { background:transparent; border:none; }"
"QScrollBar:horizontal { height:6px; background:transparent; }"
"QScrollBar::handle:horizontal { background:rgba(255,255,255,40); border-radius:3px; min-width:30px; }"
"QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width:0; }"
);
auto* strip = new QWidget();
strip->setStyleSheet("background:transparent;");
auto* hlay = new QHBoxLayout(strip);
hlay->setContentsMargins(0, 0, 0, 0);
hlay->setSpacing(12);
auto allDefs = CardDatabase::instance().allCardDefs();
QStringList order = {
"student_council_president", "class_monitor", "honor_student",
"discipline_committee_member", "library_committee_member",
"health_committee_member", "young_lady", "newspaper_club",
"go_home_club", "infected_person", "accomplice", "culprit", "alien"
};
for (const auto& id : order) {
auto* col = new QWidget(strip);
auto* colLayout = new QVBoxLayout(col);
colLayout->setContentsMargins(0, 0, 0, 0);
colLayout->setSpacing(3);
colLayout->setAlignment(Qt::AlignCenter);
auto* cw = new CardWidget(col);
cw->setCardType(id);
cw->setFaceUp(true);
cw->setCardEnabled(false);
colLayout->addWidget(cw, 0, Qt::AlignCenter);
const CardDef* def = CardDatabase::instance().getCardDef(id);
QString name = def ? def->name : id;
auto* nameLabel = new QLabel(name, col);
nameLabel->setStyleSheet("color:#aaa; font-size:10px; background:transparent;");
nameLabel->setAlignment(Qt::AlignCenter);
colLayout->addWidget(nameLabel);
hlay->addWidget(col);
}
hlay->addStretch();
scroll->setWidget(strip);
QScroller::grabGesture(scroll->viewport(), QScroller::LeftMouseButtonGesture);
sLayout->addWidget(scroll, 1);
parentLayout->addWidget(section);
}
void LobbyWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
QLinearGradient grad(0, 0, width(), height());
grad.setColorAt(0, QColor("#1a1f2e"));
grad.setColorAt(0.5, QColor("#15192a"));
grad.setColorAt(1, QColor("#1a1f2e"));
p.fillRect(rect(), grad);
}
void LobbyWidget::onConnect() {
auto& net = NetworkManager::instance();
if (net.isConnected()) {
net.disconnect();
} else {
auto url = _serverEdit->text().trimmed();
if (url.isEmpty()) return;
_connectBtn->setEnabled(false);
_connectBtn->setText(QStringLiteral("连接中..."));
net.connectToServer(url);
}
}
void LobbyWidget::onCreateRoom() {
auto nick = _nicknameEdit->text().trimmed();
if (nick.isEmpty()) nick = QStringLiteral("玩家");
NetworkManager::instance().createRoom(nick, _maxPlayersSpin->value());
}
void LobbyWidget::onJoinRoom() {
auto nick = _nicknameEdit->text().trimmed();
auto rid = _roomIdEdit->text().trimmed();
if (rid.isEmpty()) {
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请输入房间号"), 2000, this);
return;
}
if (nick.isEmpty()) nick = QStringLiteral("玩家");
NetworkManager::instance().joinRoom(rid, nick);
}
void LobbyWidget::onJoinTestRoom(int playerCount) {
auto nick = _nicknameEdit->text().trimmed();
if (nick.isEmpty()) nick = QStringLiteral("玩家");
QString roomId = QStringLiteral("TEST%1").arg(playerCount);
ElaMessageBar::information(ElaMessageBarType::Top, QStringLiteral("加入房间"), QStringLiteral("正在加入 ") + roomId + QStringLiteral(" ..."), 1500, this);
NetworkManager::instance().joinRoom(roomId, nick);
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef LOBBYWIDGET_H
#define LOBBYWIDGET_H
#include <QWidget>
class ElaLineEdit;
class ElaPushButton;
class ElaSpinBox;
class QLabel;
class QScrollArea;
class LobbyWidget : public QWidget {
Q_OBJECT
public:
explicit LobbyWidget(QWidget* parent = nullptr);
signals:
void joinedRoom();
void createdRoom();
protected:
void paintEvent(QPaintEvent* event) override;
private:
void initUI();
void buildCardGallery(QWidget* parent, QLayout* parentLayout);
void onConnect();
void onCreateRoom();
void onJoinRoom();
void onJoinTestRoom(int playerCount);
ElaLineEdit* _serverEdit = nullptr;
ElaLineEdit* _nicknameEdit = nullptr;
ElaLineEdit* _roomIdEdit = nullptr;
ElaSpinBox* _maxPlayersSpin = nullptr;
ElaPushButton* _connectBtn = nullptr;
ElaPushButton* _createBtn = nullptr;
ElaPushButton* _joinBtn = nullptr;
QLabel* _statusLabel = nullptr;
};
#endif
+43
View File
@@ -0,0 +1,43 @@
#include "MainWindow.h"
#include "LobbyWidget.h"
#include "RoomWidget.h"
#include "GameWidget.h"
#include <QStackedWidget>
#include <QVBoxLayout>
MainWindow::MainWindow(QWidget* parent) : QWidget(parent)
{
initUI();
}
void MainWindow::initUI() {
setWindowTitle(QStringLiteral("冰冷的她醒来之前"));
setWindowIcon(QIcon(":/images/corpse"));
resize(1200, 800);
setMinimumSize(900, 600);
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
_stack = new QStackedWidget(this);
_lobby = new LobbyWidget(this);
_room = new RoomWidget(this);
_game = new GameWidget(this);
_stack->addWidget(_lobby);
_stack->addWidget(_room);
_stack->addWidget(_game);
_stack->setCurrentWidget(_lobby);
layout->addWidget(_stack);
connect(_lobby, &LobbyWidget::createdRoom, this, &MainWindow::switchToRoom);
connect(_lobby, &LobbyWidget::joinedRoom, this, &MainWindow::switchToRoom);
connect(_room, &RoomWidget::gameStarted, this, &MainWindow::switchToGame);
connect(_room, &RoomWidget::leftRoom, this, &MainWindow::switchToLobby);
connect(_game, &GameWidget::gameFinished, this, &MainWindow::switchToRoom);
}
void MainWindow::switchToLobby() { _stack->setCurrentWidget(_lobby); }
void MainWindow::switchToRoom() { _stack->setCurrentWidget(_room); }
void MainWindow::switchToGame() { _stack->setCurrentWidget(_game); }
+29
View File
@@ -0,0 +1,29 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QWidget>
class QStackedWidget;
class LobbyWidget;
class RoomWidget;
class GameWidget;
class MainWindow : public QWidget {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
void switchToLobby();
void switchToRoom();
void switchToGame();
private:
void initUI();
QStackedWidget* _stack = nullptr;
LobbyWidget* _lobby = nullptr;
RoomWidget* _room = nullptr;
GameWidget* _game = nullptr;
};
#endif
+126
View File
@@ -0,0 +1,126 @@
#include "PlayerSeatWidget.h"
#include "CardWidget.h"
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPainter>
#include <QPainterPath>
#include <QJsonArray>
#include <QMouseEvent>
PlayerSeatWidget::PlayerSeatWidget(QWidget* parent)
: QWidget(parent)
{
initUI();
setFixedSize(180, 130);
}
void PlayerSeatWidget::initUI() {
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(10, 8, 10, 8);
mainLayout->setSpacing(4);
_nameLabel = new QLabel(this);
_nameLabel->setStyleSheet("color: white; font-size: 14px; font-weight: bold;");
_nameLabel->setAlignment(Qt::AlignCenter);
mainLayout->addWidget(_nameLabel);
auto* infoRow = new QHBoxLayout();
infoRow->setSpacing(8);
_handCountLabel = new QLabel(this);
_handCountLabel->setStyleSheet("color: #aaa; font-size: 11px;");
_challengeLabel = new QLabel(this);
_challengeLabel->setStyleSheet("color: #ff6b6b; font-size: 11px;");
_statusLabel = new QLabel(this);
_statusLabel->setStyleSheet("color: #4ecdc4; font-size: 11px;");
infoRow->addWidget(_handCountLabel);
infoRow->addWidget(_challengeLabel);
infoRow->addStretch();
infoRow->addWidget(_statusLabel);
mainLayout->addLayout(infoRow);
_skillZoneWidget = new QWidget(this);
_skillLayout = new QHBoxLayout(_skillZoneWidget);
_skillLayout->setContentsMargins(0, 0, 0, 0);
_skillLayout->setSpacing(2);
_skillLayout->addStretch();
mainLayout->addWidget(_skillZoneWidget);
mainLayout->addStretch();
}
void PlayerSeatWidget::setPlayerData(const QJsonObject& data) {
_playerId = data["player_id"].toString();
auto nick = data["nickname"].toString();
auto handCount = data["hand_card_count"].toInt();
auto challengeCount = data["challenge_zone_count"].toInt();
_isExited = data["is_exited"].toBool();
_nameLabel->setText(nick);
_handCountLabel->setText(QStringLiteral("手牌: %1").arg(handCount));
if (challengeCount > 0) {
_challengeLabel->setText(QStringLiteral("质疑: %1").arg(challengeCount));
_challengeLabel->show();
} else {
_challengeLabel->hide();
}
if (_isExited) {
_statusLabel->setText(QStringLiteral("已退出"));
_statusLabel->show();
} else {
_statusLabel->hide();
}
updateSkillZone(data["skill_zone"].toArray());
update();
}
void PlayerSeatWidget::setIsCurrentTurn(bool isTurn) {
_isTurn = isTurn;
update();
}
void PlayerSeatWidget::setPosition(int position) {
_position = position;
}
void PlayerSeatWidget::updateSkillZone(const QJsonArray& cards) {
for (auto* c : _skillCards) {
_skillLayout->removeWidget(c);
c->deleteLater();
}
_skillCards.clear();
for (const auto& cv : cards) {
auto c = cv.toObject();
auto* card = new CardWidget(_skillZoneWidget);
card->setMini(true);
card->setCardType(c["type_id"].toString());
card->setFaceUp(true);
card->setCardEnabled(false);
_skillLayout->insertWidget(_skillLayout->count() - 1, card);
_skillCards.append(card);
}
}
void PlayerSeatWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
QColor bgColor = _isTurn ? QColor(94, 179, 230, 30) : QColor(37, 43, 61, 180);
QColor borderColor = _isTurn ? QColor("#5eb3e6") : QColor(255, 255, 255, 25);
QPainterPath path;
path.addRoundedRect(rect().adjusted(1, 1, -1, -1), 10, 10);
p.fillPath(path, bgColor);
p.setPen(QPen(borderColor, _isTurn ? 2.0 : 1.0));
p.drawPath(path);
}
void PlayerSeatWidget::mousePressEvent(QMouseEvent* e) {
if (e->button() == Qt::LeftButton) {
emit playerClicked(_playerId);
}
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef PLAYERSEATWIDGET_H
#define PLAYERSEATWIDGET_H
#include <QWidget>
#include <QJsonObject>
class QLabel;
class QHBoxLayout;
class CardWidget;
class PlayerSeatWidget : public QWidget {
Q_OBJECT
public:
explicit PlayerSeatWidget(QWidget* parent = nullptr);
void setPlayerData(const QJsonObject& data);
void setIsCurrentTurn(bool isTurn);
void setPosition(int position);
QString playerId() const { return _playerId; }
signals:
void playerClicked(const QString& playerId);
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
private:
void initUI();
void updateSkillZone(const QJsonArray& cards);
QString _playerId;
int _position = 0;
bool _isTurn = false;
bool _isExited = false;
QLabel* _nameLabel = nullptr;
QLabel* _handCountLabel = nullptr;
QLabel* _challengeLabel = nullptr;
QLabel* _statusLabel = nullptr;
QHBoxLayout* _skillLayout = nullptr;
QWidget* _skillZoneWidget = nullptr;
QVector<CardWidget*> _skillCards;
};
#endif
+167
View File
@@ -0,0 +1,167 @@
#include "RoomWidget.h"
#include "ElaPushButton.h"
#include "ElaToggleSwitch.h"
#include "ElaMessageBar.h"
#include "NetworkManager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPainter>
#include <QLinearGradient>
#include <QJsonArray>
#include <QClipboard>
#include <QApplication>
RoomWidget::RoomWidget(QWidget* parent) : QWidget(parent)
{
initUI();
auto& net = NetworkManager::instance();
connect(&net, &NetworkManager::roomStateUpdated, this, &RoomWidget::onRoomState);
connect(&net, &NetworkManager::gameStarted, this, [this](const QJsonObject&) { emit gameStarted(); });
connect(&net, &NetworkManager::roomLeft, this, [this]() { emit leftRoom(); });
}
void RoomWidget::initUI() {
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setAlignment(Qt::AlignCenter);
auto* card = new QWidget(this);
card->setFixedSize(520, 580);
card->setObjectName("roomCard");
card->setStyleSheet("QWidget#roomCard { background: rgba(37,43,61,230); border-radius: 16px; border: 1px solid rgba(94,179,230,40); }");
auto* layout = new QVBoxLayout(card);
layout->setContentsMargins(30, 20, 30, 20);
layout->setSpacing(10);
auto* headerLabel = new QLabel(QStringLiteral("— 等待室 —"), card);
headerLabel->setStyleSheet("color: #4ecdc4; font-size: 12px; letter-spacing: 6px; background:transparent;");
headerLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(headerLabel);
_roomIdLabel = new QLabel(card);
_roomIdLabel->setStyleSheet("color: #ffd700; font-size: 28px; font-weight: bold; background:transparent;");
_roomIdLabel->setAlignment(Qt::AlignCenter);
_roomIdLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
_roomIdLabel->setCursor(Qt::IBeamCursor);
_roomIdLabel->setToolTip(QStringLiteral("点击可选中复制"));
layout->addWidget(_roomIdLabel);
_infoLabel = new QLabel(card);
_infoLabel->setStyleSheet("color: #888; font-size: 13px; background:transparent;");
_infoLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(_infoLabel);
layout->addSpacing(5);
auto* listHeader = new QLabel(QStringLiteral("玩家"), card);
listHeader->setStyleSheet("color: #bbb; font-size: 13px; font-weight: bold; background:transparent;");
layout->addWidget(listHeader);
_playerList = new QListWidget(card);
_playerList->setMinimumHeight(220);
_playerList->setStyleSheet(
"QListWidget { background: rgba(255,255,255,5); border: 1px solid rgba(255,255,255,12); border-radius: 8px; color: white; font-size: 14px; }"
"QListWidget::item { padding: 10px 15px; border-bottom: 1px solid rgba(255,255,255,6); }"
);
layout->addWidget(_playerList, 1);
auto* chaosRow = new QHBoxLayout();
auto* chaosLbl = new QLabel(QStringLiteral("混沌模式"), card);
chaosLbl->setStyleSheet("color: #999; font-size: 12px; background:transparent;");
_chaosSwitch = new ElaToggleSwitch(card);
chaosRow->addWidget(chaosLbl);
chaosRow->addStretch();
chaosRow->addWidget(_chaosSwitch);
layout->addLayout(chaosRow);
auto* btnRow = new QHBoxLayout();
btnRow->setSpacing(10);
_leaveBtn = new ElaPushButton(QStringLiteral("离开房间"), card);
_leaveBtn->setFixedHeight(38);
connect(_leaveBtn, &ElaPushButton::clicked, this, [this]() { NetworkManager::instance().leaveRoom(); });
_readyBtn = new ElaPushButton(QStringLiteral("准备"), card);
_readyBtn->setFixedHeight(38);
connect(_readyBtn, &ElaPushButton::clicked, this, [this]() {
_isReady = !_isReady;
NetworkManager::instance().setReady(_isReady);
});
_startBtn = new ElaPushButton(QStringLiteral("开始游戏"), card);
_startBtn->setFixedHeight(38);
_startBtn->setEnabled(false);
_startBtn->hide();
connect(_startBtn, &ElaPushButton::clicked, this, [this]() { NetworkManager::instance().startGame(); });
btnRow->addWidget(_leaveBtn);
btnRow->addStretch();
btnRow->addWidget(_readyBtn);
btnRow->addWidget(_startBtn);
layout->addLayout(btnRow);
root->addWidget(card);
}
void RoomWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
QLinearGradient grad(0, 0, width(), height());
grad.setColorAt(0, QColor("#1a1f2e"));
grad.setColorAt(0.5, QColor("#1d2233"));
grad.setColorAt(1, QColor("#1a1f2e"));
p.fillRect(rect(), grad);
}
void RoomWidget::onRoomState(const QJsonObject& state) {
auto roomId = state["room_id"].toString();
_roomIdLabel->setText(roomId);
auto ht = state["harmony_target"].toInt();
int total = state["players"].toArray().size();
QString info = QStringLiteral("%1 / %2 人").arg(total).arg(total);
if (ht > 0) info += QStringLiteral(" | 调和目标: %1").arg(ht);
_infoLabel->setText(info);
_localPlayerId = state["your_player_id"].toString();
_playerList->clear();
auto players = state["players"].toArray();
bool allReady = !players.isEmpty();
_isHost = false;
for (const auto& pv : players) {
auto p = pv.toObject();
auto pid = p["player_id"].toString();
auto nick = p["nickname"].toString();
auto ready = p["is_ready"].toBool();
auto host = p["is_host"].toBool();
auto bot = p["is_bot"].toBool();
QString text;
if (bot) {
text = QStringLiteral("🤖 %1").arg(nick);
} else {
text = QStringLiteral("👤 %1").arg(nick);
}
if (host) text += QStringLiteral(" 👑 房主");
if (ready) text += QStringLiteral(" ✔ 已准备");
else text += QStringLiteral(" ⏳ 未准备");
if (pid == _localPlayerId) text += QStringLiteral(" ← 你");
if (!ready && !bot) allReady = false;
if (pid == _localPlayerId && host) _isHost = true;
if (pid == _localPlayerId) _isReady = ready;
auto* item = new QListWidgetItem(text);
if (pid == _localPlayerId)
item->setForeground(QColor("#4ecdc4"));
else if (bot)
item->setForeground(QColor("#888"));
_playerList->addItem(item);
}
_readyBtn->setText(_isReady ? QStringLiteral("取消准备") : QStringLiteral("准备"));
_startBtn->setVisible(_isHost);
_startBtn->setEnabled(_isHost && allReady && players.size() >= 3);
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef ROOMWIDGET_H
#define ROOMWIDGET_H
#include <QWidget>
#include <QJsonObject>
class ElaPushButton;
class QLabel;
class QListWidget;
class ElaToggleSwitch;
class RoomWidget : public QWidget {
Q_OBJECT
public:
explicit RoomWidget(QWidget* parent = nullptr);
signals:
void gameStarted();
void leftRoom();
protected:
void paintEvent(QPaintEvent* event) override;
private:
void initUI();
void onRoomState(const QJsonObject& state);
QLabel* _titleLabel = nullptr;
QLabel* _roomIdLabel = nullptr;
QLabel* _infoLabel = nullptr;
QListWidget* _playerList = nullptr;
ElaPushButton* _readyBtn = nullptr;
ElaPushButton* _startBtn = nullptr;
ElaPushButton* _leaveBtn = nullptr;
ElaToggleSwitch* _chaosSwitch = nullptr;
bool _isReady = false;
bool _isHost = false;
QString _localPlayerId;
};
#endif
+172
View File
@@ -0,0 +1,172 @@
#include "SceneCardItem.h"
#include "CardData.h"
#include <QPainter>
#include <QPainterPath>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsSceneHoverEvent>
#include <QPropertyAnimation>
#include <QCursor>
#include <QtMath>
SceneCardItem::SceneCardItem(QGraphicsItem* parent)
: QGraphicsObject(parent)
{
setAcceptHoverEvents(true);
setCacheMode(DeviceCoordinateCache);
auto backRaw = CardDatabase::instance().getCardBackImage();
if (!backRaw.isNull())
_scaledBack = backRaw.scaled(int(W), int(H), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)
.copy(0, 0, int(W), int(H));
}
void SceneCardItem::setCardData(const QString& uid, const QString& typeId, bool faceUp) {
_uid = uid;
_typeId = typeId;
_faceUp = faceUp;
rebuildPixmap();
update();
}
void SceneCardItem::setFaceUp(bool f) { _faceUp = f; update(); }
void SceneCardItem::setCardSelected(bool s) {
if (_selected == s) return;
bool was = _selected;
_selected = s;
if (s && !was) setY(y() - 20);
else if (!s && was) setY(y() + 20);
update();
}
void SceneCardItem::setCardEnabled(bool e) {
_enabled = e;
setCursor(e ? Qt::PointingHandCursor : Qt::ArrowCursor);
update();
}
QRectF SceneCardItem::boundingRect() const {
return {-8, -8, W + 16, H + 16};
}
void SceneCardItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
p->setRenderHint(QPainter::Antialiasing);
p->setRenderHint(QPainter::SmoothPixmapTransform);
QRectF cr(0, 0, W, H);
constexpr qreal r = 8;
if (_hovered && _enabled) {
QRadialGradient glow(cr.center(), W * 0.7);
glow.setColorAt(0, QColor(94, 179, 230, 60));
glow.setColorAt(1, Qt::transparent);
p->setPen(Qt::NoPen);
p->setBrush(glow);
p->drawRoundedRect(cr.adjusted(-6, -6, 6, 6), r + 4, r + 4);
}
QPainterPath clip;
clip.addRoundedRect(cr, r, r);
p->setClipPath(clip);
bool showFront = _showFlippedFace ? !_faceUp : _faceUp;
QPixmap& px = (showFront && !_scaledFace.isNull()) ? _scaledFace : _scaledBack;
if (!px.isNull()) {
p->drawPixmap(cr.toRect(), px);
} else {
QLinearGradient bg(0, 0, 0, H);
if (showFront) { bg.setColorAt(0, QColor("#d4c5a3")); bg.setColorAt(1, QColor("#b8a67a")); }
else { bg.setColorAt(0, QColor("#2a3a5a")); bg.setColorAt(1, QColor("#1a2a44")); }
p->fillRect(cr, bg);
}
p->setClipping(false);
if (showFront) {
const CardDef* def = CardDatabase::instance().getCardDef(_typeId);
if (def) {
p->setPen(Qt::NoPen);
p->setBrush(QColor(0, 0, 0, 160));
QRectF badge(3, 3, 26, 20);
p->drawRoundedRect(badge, 4, 4);
p->setFont(QFont("Consolas", 11, QFont::Bold));
p->setPen(def->mp >= 0 ? QColor("#d4c5a3") : QColor("#d94f5c"));
p->drawText(badge, Qt::AlignCenter, QString::number(def->mp));
}
}
QPen border;
if (_selected) border = QPen(QColor("#5eb3e6"), 2.5);
else if (_hovered && _enabled) border = QPen(QColor(94, 179, 230, 120), 1.5);
else border = QPen(QColor("#8b2635"), 1);
p->setPen(border);
p->setBrush(Qt::NoBrush);
p->drawRoundedRect(cr.adjusted(0.5, 0.5, -0.5, -0.5), r, r);
if (!_enabled) {
QPainterPath dc;
dc.addRoundedRect(cr, r, r);
p->setClipPath(dc);
p->fillRect(cr, QColor(0, 0, 0, 100));
}
if (_hlOpacity > 0.01) {
QPainterPath hc;
hc.addRoundedRect(cr, r, r);
p->setClipPath(hc);
p->fillRect(cr, QColor(94, 179, 230, int(40 * _hlOpacity)));
}
}
void SceneCardItem::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
if (!_enabled) return;
_hovered = true;
update();
emit hoverIn(_typeId);
}
void SceneCardItem::hoverLeaveEvent(QGraphicsSceneHoverEvent*) {
_hovered = false;
update();
emit hoverOut();
}
void SceneCardItem::mousePressEvent(QGraphicsSceneMouseEvent* e) {
if (e->button() == Qt::LeftButton && _enabled)
emit clicked(_uid);
if (e->button() == Qt::RightButton && _faceUp)
emit rightClicked(_typeId);
}
void SceneCardItem::setFlipProgress(qreal p) {
_flipProg = p;
qreal scaleX = qMax(0.02, qAbs(qCos(p * M_PI)));
QTransform t;
t.translate(W / 2.0, H / 2.0);
t.scale(scaleX, 1.0);
t.translate(-W / 2.0, -H / 2.0);
setTransform(t);
_showFlippedFace = (p >= 0.5);
update();
}
void SceneCardItem::animateFlip(bool toFaceUp, int duration) {
auto* anim = new QPropertyAnimation(this, "flipProgress");
anim->setDuration(duration);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::InOutQuad);
connect(anim, &QPropertyAnimation::finished, this, [this, toFaceUp]() {
_faceUp = toFaceUp;
_flipProg = 0;
_showFlippedFace = false;
setTransform(QTransform());
update();
});
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
void SceneCardItem::rebuildPixmap() {
if (!_typeId.isEmpty()) {
auto raw = CardDatabase::instance().getCardFrontImage(_typeId);
if (!raw.isNull())
_scaledFace = raw.scaled(int(W), int(H), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)
.copy(0, 0, int(W), int(H));
}
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef SCENECARDITEM_H
#define SCENECARDITEM_H
#include <QGraphicsObject>
#include <QPixmap>
class SceneCardItem : public QGraphicsObject {
Q_OBJECT
Q_PROPERTY(qreal highlightOpacity READ highlightOpacity WRITE setHighlightOpacity)
Q_PROPERTY(qreal flipProgress READ flipProgress WRITE setFlipProgress)
public:
explicit SceneCardItem(QGraphicsItem* parent = nullptr);
void setCardData(const QString& uid, const QString& typeId, bool faceUp);
void setFaceUp(bool f);
void setCardSelected(bool s);
void setCardEnabled(bool e);
QString uid() const { return _uid; }
QString typeId() const { return _typeId; }
bool isFaceUp() const { return _faceUp; }
bool isCardSelected() const { return _selected; }
qreal highlightOpacity() const { return _hlOpacity; }
void setHighlightOpacity(qreal o) { _hlOpacity = o; update(); }
qreal flipProgress() const { return _flipProg; }
void setFlipProgress(qreal p);
void animateFlip(bool toFaceUp, int duration = 500);
static constexpr qreal W = 100;
static constexpr qreal H = 140;
QRectF boundingRect() const override;
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override;
signals:
void clicked(const QString& uid);
void rightClicked(const QString& typeId);
void hoverIn(const QString& typeId);
void hoverOut();
protected:
void hoverEnterEvent(QGraphicsSceneHoverEvent* e) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent* e) override;
void mousePressEvent(QGraphicsSceneMouseEvent* e) override;
private:
void rebuildPixmap();
QString _uid, _typeId;
bool _faceUp = false;
bool _selected = false;
bool _enabled = true;
bool _hovered = false;
qreal _hlOpacity = 0;
qreal _flipProg = 0;
bool _showFlippedFace = false;
QPixmap _scaledFace, _scaledBack;
};
#endif
+322
View File
@@ -0,0 +1,322 @@
#include "SettlementOverlay.h"
#include "CardWidget.h"
#include "CardData.h"
#include "ElaPushButton.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QScrollArea>
#include <QJsonArray>
#include <QSet>
#include <QPropertyAnimation>
static const QColor S_ACCENT("#5eb3e6");
static const QColor S_SUCCESS("#5cb85c");
static const QColor S_DANGER("#d94f5c");
static const QColor S_GOLD("#d4c5a3");
static const QColor S_TEXT("#e0e0e0");
static const QColor S_DIM("#8a8f9d");
SettlementOverlay::SettlementOverlay(QWidget* parent) : QWidget(parent)
{
hide();
_timer = new QTimer(this);
_timer->setInterval(500);
connect(_timer, &QTimer::timeout, this, &SettlementOverlay::revealNextCard);
_panel = new QWidget(this);
_panel->setFixedSize(800, 600);
_panel->setObjectName("stPanel");
_panel->setStyleSheet("QWidget#stPanel{background:rgba(20,24,38,245);border-radius:16px;border:1px solid rgba(94,179,230,40);}");
auto* pLay = new QVBoxLayout(_panel);
pLay->setContentsMargins(30, 20, 30, 20);
pLay->setSpacing(8);
_phaseTitle = new QLabel(_panel);
_phaseTitle->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(S_GOLD.name()));
_phaseTitle->setAlignment(Qt::AlignCenter);
pLay->addWidget(_phaseTitle);
_scroll = new QScrollArea(_panel);
_scroll->setWidgetResizable(true);
_scroll->setStyleSheet("QScrollArea{background:transparent;border:none;}QScrollBar{width:4px;background:transparent;}QScrollBar::handle{background:rgba(255,255,255,30);border-radius:2px;}");
_content = new QWidget();
_content->setStyleSheet("background:transparent;");
_contentLayout = new QVBoxLayout(_content);
_contentLayout->setContentsMargins(0, 0, 0, 0);
_contentLayout->setSpacing(8);
_contentLayout->setAlignment(Qt::AlignTop);
_scroll->setWidget(_content);
pLay->addWidget(_scroll, 1);
_totalLabel = new QLabel(_panel);
_totalLabel->setStyleSheet(QStringLiteral("color:%1;font-size:16px;font-weight:bold;background:transparent;").arg(S_ACCENT.name()));
_totalLabel->setAlignment(Qt::AlignCenter);
pLay->addWidget(_totalLabel);
_resultLabel = new QLabel(_panel);
_resultLabel->setStyleSheet(QStringLiteral("color:%1;font-size:20px;font-weight:bold;background:transparent;").arg(S_GOLD.name()));
_resultLabel->setAlignment(Qt::AlignCenter);
_resultLabel->hide();
pLay->addWidget(_resultLabel);
_nextBtn = new ElaPushButton(QStringLiteral("继续"), _panel);
_nextBtn->setFixedSize(120, 38);
_nextBtn->hide();
connect(_nextBtn, &ElaPushButton::clicked, this, [this]() { startPhase(_phase + 1); });
pLay->addWidget(_nextBtn, 0, Qt::AlignCenter);
}
void SettlementOverlay::showSettlement(const QJsonObject& data) {
_data = data;
show();
raise();
_panel->move((width() - 800) / 2, (height() - 600) / 2);
startPhase(0);
}
void SettlementOverlay::paintEvent(QPaintEvent*) {
QPainter p(this);
p.fillRect(rect(), QColor(0, 0, 0, 190));
}
void SettlementOverlay::startPhase(int phase) {
_phase = phase;
_revealIdx = 0;
_runningTotal = 0;
_currentPlayerIdx = 0;
_timer->stop();
_nextBtn->hide();
_resultLabel->hide();
_totalLabel->clear();
while (_contentLayout->count()) {
auto* item = _contentLayout->takeAt(0);
if (item->widget()) item->widget()->deleteLater();
delete item;
}
_cardRow = nullptr;
_cardRowWidget = nullptr;
if (phase == 0) {
_phaseTitle->setText(QStringLiteral("— 调和判定 —"));
_phaseTitle->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(S_ACCENT.name()));
auto h = _data["harmony"].toObject();
_totalLabel->setText(QStringLiteral("目标: %1 | 合计: 0").arg(h["target"].toInt()));
_cardRowWidget = new QWidget(_content);
_cardRowWidget->setStyleSheet("background:transparent;");
_cardRow = new QHBoxLayout(_cardRowWidget);
_cardRow->setAlignment(Qt::AlignCenter);
_cardRow->setSpacing(8);
_contentLayout->addWidget(_cardRowWidget);
_timer->start();
} else if (phase == 1) {
_phaseTitle->setText(QStringLiteral("— 质疑判定 —"));
_phaseTitle->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(QColor("#ff8c42").name()));
_totalLabel->clear();
_timer->start();
} else if (phase == 2) {
_phaseTitle->setText(QStringLiteral("— 胜利判定 —"));
_phaseTitle->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(S_GOLD.name()));
_totalLabel->clear();
_timer->setInterval(800);
_timer->start();
} else {
buildFinalPanel();
}
}
void SettlementOverlay::revealNextCard() {
if (_phase == 0) {
auto cards = _data["harmony"].toObject()["cards"].toArray();
if (_revealIdx >= cards.size()) {
_timer->stop();
showPhaseResult();
return;
}
auto c = cards[_revealIdx].toObject();
auto* cw = makeFlipCard(c);
_cardRow->addWidget(cw);
_runningTotal += c["mp"].toInt();
int target = _data["harmony"].toObject()["target"].toInt();
_totalLabel->setText(QStringLiteral("目标: %1 | 合计: %2").arg(target).arg(_runningTotal));
_revealIdx++;
} else if (_phase == 1) {
auto perPlayer = _data["challenge"].toObject()["per_player"].toArray();
if (_currentPlayerIdx >= perPlayer.size()) {
_timer->stop();
showPhaseResult();
return;
}
auto pp = perPlayer[_currentPlayerIdx].toObject();
auto cards = pp["cards"].toArray();
if (_revealIdx == 0) {
auto* nameLabel = new QLabel(QStringLiteral("%1 (质疑合计: %2)").arg(pp["nickname"].toString()).arg(pp["total_mp"].toInt()), _content);
nameLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;background:transparent;").arg(S_TEXT.name()));
_contentLayout->addWidget(nameLabel);
_cardRowWidget = new QWidget(_content);
_cardRowWidget->setStyleSheet("background:transparent;");
_cardRow = new QHBoxLayout(_cardRowWidget);
_cardRow->setAlignment(Qt::AlignLeft);
_cardRow->setSpacing(6);
_contentLayout->addWidget(_cardRowWidget);
}
if (_revealIdx < cards.size()) {
auto* cw = makeFlipCard(cards[_revealIdx].toObject());
_cardRow->addWidget(cw);
_revealIdx++;
} else {
if (cards.isEmpty()) {
auto* empty = new QLabel(QStringLiteral(" (无质疑牌)"), _content);
empty->setStyleSheet(QStringLiteral("color:%1;font-size:12px;background:transparent;").arg(S_DIM.name()));
_contentLayout->addWidget(empty);
}
_currentPlayerIdx++;
_revealIdx = 0;
}
} else if (_phase == 2) {
auto checks = _data["victory"].toObject()["checks"].toArray();
if (_revealIdx >= checks.size()) {
_timer->stop();
showPhaseResult();
return;
}
auto vc = checks[_revealIdx].toObject();
bool met = vc["met"].toBool();
auto* label = new QLabel(QStringLiteral("优先%1 %2 [%3] → %4")
.arg(vc["priority"].toInt())
.arg(vc["nickname"].toString())
.arg(vc["card_name"].toString())
.arg(met ? QStringLiteral("✓ 达成") : QStringLiteral("✗ 未达成")),
_content);
label->setStyleSheet(met
? QStringLiteral("color:%1;font-size:14px;font-weight:bold;background:transparent;").arg(S_SUCCESS.name())
: QStringLiteral("color:%1;font-size:14px;background:transparent;").arg(S_DIM.name()));
_contentLayout->addWidget(label);
_revealIdx++;
}
}
void SettlementOverlay::showPhaseResult() {
_resultLabel->show();
_nextBtn->show();
if (_phase == 0) {
bool ok = _data["harmony"].toObject()["success"].toBool();
_resultLabel->setText(ok ? QStringLiteral("调和成功 ✓") : QStringLiteral("调和失败 ✗"));
_resultLabel->setStyleSheet(QStringLiteral("color:%1;font-size:24px;font-weight:bold;background:transparent;")
.arg(ok ? S_SUCCESS.name() : S_DANGER.name()));
} else if (_phase == 1) {
auto imprisoned = _data["challenge"].toObject()["imprisoned"].toArray();
if (imprisoned.isEmpty()) {
_resultLabel->setText(QStringLiteral("无人被监禁"));
_resultLabel->setStyleSheet(QStringLiteral("color:%1;font-size:20px;font-weight:bold;background:transparent;").arg(S_DIM.name()));
} else {
QStringList names;
auto pp = _data["challenge"].toObject()["per_player"].toArray();
QSet<QString> jailed;
for (const auto& v : imprisoned) jailed.insert(v.toString());
for (const auto& pv : pp)
if (jailed.contains(pv.toObject()["player_id"].toString()))
names.append(pv.toObject()["nickname"].toString());
_resultLabel->setText(QStringLiteral("🔒 被监禁: ") + names.join(QStringLiteral(", ")));
_resultLabel->setStyleSheet(QStringLiteral("color:%1;font-size:20px;font-weight:bold;background:transparent;").arg(S_DANGER.name()));
}
} else if (_phase == 2) {
auto v = _data["victory"].toObject();
if (v["end_type"].toString() == "all_dead") {
_resultLabel->setText(QStringLiteral("全灭结局 — 没有任何人获胜"));
_resultLabel->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(S_DANGER.name()));
} else {
QStringList names;
for (const auto& wv : v["winners"].toArray())
names.append(wv.toObject()["nickname"].toString());
_resultLabel->setText(QStringLiteral("🏆 获胜: ") + names.join(QStringLiteral(", ")));
_resultLabel->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(S_GOLD.name()));
}
_nextBtn->setText(QStringLiteral("查看详情"));
}
}
void SettlementOverlay::buildFinalPanel() {
_phaseTitle->setText(QStringLiteral("— 结算完毕 —"));
_phaseTitle->setStyleSheet(QStringLiteral("color:%1;font-size:22px;font-weight:bold;background:transparent;").arg(S_GOLD.name()));
_resultLabel->hide();
_totalLabel->clear();
auto v = _data["victory"].toObject();
QString endText;
if (v["end_type"].toString() == "all_dead")
endText = QStringLiteral("全灭结局");
else {
QStringList n;
for (const auto& w : v["winners"].toArray()) n.append(w.toObject()["nickname"].toString());
endText = QStringLiteral("获胜者: ") + n.join(", ");
}
auto* endLabel = new QLabel(endText, _content);
endLabel->setStyleSheet(QStringLiteral("color:%1;font-size:18px;font-weight:bold;background:transparent;").arg(S_GOLD.name()));
endLabel->setAlignment(Qt::AlignCenter);
_contentLayout->addWidget(endLabel);
_contentLayout->addSpacing(10);
auto checks = v["checks"].toArray();
for (const auto& cv : checks) {
auto c = cv.toObject();
auto* row = new QLabel(QStringLiteral("%1 [%2] 优先%3 — %4")
.arg(c["nickname"].toString(), c["card_name"].toString())
.arg(c["priority"].toInt())
.arg(c["met"].toBool() ? QStringLiteral("") : QStringLiteral("")),
_content);
row->setStyleSheet(c["met"].toBool()
? QStringLiteral("color:%1;font-size:13px;background:transparent;").arg(S_SUCCESS.name())
: QStringLiteral("color:%1;font-size:13px;background:transparent;").arg(S_DIM.name()));
_contentLayout->addWidget(row);
}
_contentLayout->addStretch();
auto* btnRow = new QWidget(_content);
btnRow->setStyleSheet("background:transparent;");
auto* bLay = new QHBoxLayout(btnRow);
bLay->setAlignment(Qt::AlignCenter);
bLay->setSpacing(16);
auto* backBtn = new ElaPushButton(QStringLiteral("返回等待室"), btnRow);
backBtn->setFixedSize(140, 38);
connect(backBtn, &ElaPushButton::clicked, this, [this]() { hide(); emit finished(); });
bLay->addWidget(backBtn);
_contentLayout->addWidget(btnRow);
_nextBtn->hide();
}
QWidget* SettlementOverlay::makeFlipCard(const QJsonObject& card) {
auto* col = new QWidget();
col->setStyleSheet("background:transparent;");
auto* vlay = new QVBoxLayout(col);
vlay->setContentsMargins(0, 0, 0, 0);
vlay->setSpacing(2);
vlay->setAlignment(Qt::AlignCenter);
auto* cw = new CardWidget(col);
cw->setMini(true);
cw->setCardType(card["type_id"].toString());
cw->setFaceUp(true);
cw->setCardEnabled(false);
vlay->addWidget(cw, 0, Qt::AlignCenter);
const CardDef* def = CardDatabase::instance().getCardDef(card["type_id"].toString());
QString name = def ? def->name : card["name"].toString();
int mp = card["mp"].toInt();
auto* lbl = new QLabel(QStringLiteral("%1 (%2)").arg(name).arg(mp), col);
lbl->setStyleSheet(QStringLiteral("color:%1;font-size:10px;background:transparent;").arg(S_TEXT.name()));
lbl->setAlignment(Qt::AlignCenter);
vlay->addWidget(lbl);
return col;
}
+55
View File
@@ -0,0 +1,55 @@
#ifndef SETTLEMENTOVERLAY_H
#define SETTLEMENTOVERLAY_H
#include <QWidget>
#include <QJsonObject>
#include <QJsonArray>
#include <QTimer>
class QVBoxLayout;
class QHBoxLayout;
class QLabel;
class ElaPushButton;
class CardWidget;
class QScrollArea;
class SettlementOverlay : public QWidget {
Q_OBJECT
public:
explicit SettlementOverlay(QWidget* parent = nullptr);
void showSettlement(const QJsonObject& data);
signals:
void finished();
protected:
void paintEvent(QPaintEvent* event) override;
private:
void startPhase(int phase);
void revealNextCard();
void showPhaseResult();
void buildFinalPanel();
QWidget* makeFlipCard(const QJsonObject& card);
QJsonObject _data;
int _phase = 0;
int _revealIdx = 0;
int _runningTotal = 0;
int _currentPlayerIdx = 0;
QTimer* _timer = nullptr;
QWidget* _panel = nullptr;
QScrollArea* _scroll = nullptr;
QWidget* _content = nullptr;
QVBoxLayout* _contentLayout = nullptr;
QLabel* _phaseTitle = nullptr;
QLabel* _totalLabel = nullptr;
QLabel* _resultLabel = nullptr;
QHBoxLayout* _cardRow = nullptr;
QWidget* _cardRowWidget = nullptr;
ElaPushButton* _nextBtn = nullptr;
QVector<QWidget*> _tempWidgets;
};
#endif
+97
View File
@@ -0,0 +1,97 @@
#include "VoiceSettingsDialog.h"
#include "VoiceManager.h"
#include "ElaPushButton.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QRadioButton>
#include <QGroupBox>
#include <QMediaDevices>
#include <QAudioDevice>
VoiceSettingsDialog::VoiceSettingsDialog(QWidget* parent)
: QDialog(parent)
{
setWindowTitle(QStringLiteral("语音设置"));
setFixedSize(400, 340);
setStyleSheet("QDialog{background:#1a1f2e;} QLabel{color:#e0e0e0;} QGroupBox{color:#8a8f9d;border:1px solid #3a3f4e;border-radius:6px;margin-top:8px;padding-top:14px;} QGroupBox::title{subcontrol-origin:margin;left:10px;}");
auto* lay = new QVBoxLayout(this);
lay->setSpacing(12);
auto* inputGroup = new QGroupBox(QStringLiteral("输入设备 (麦克风)"), this);
auto* igLay = new QVBoxLayout(inputGroup);
_inputCombo = new QComboBox(inputGroup);
igLay->addWidget(_inputCombo);
lay->addWidget(inputGroup);
auto* outputGroup = new QGroupBox(QStringLiteral("输出设备 (扬声器)"), this);
auto* ogLay = new QVBoxLayout(outputGroup);
_outputCombo = new QComboBox(outputGroup);
ogLay->addWidget(_outputCombo);
lay->addWidget(outputGroup);
auto* modeGroup = new QGroupBox(QStringLiteral("说话模式"), this);
auto* mgLay = new QVBoxLayout(modeGroup);
_pttRadio = new QRadioButton(QStringLiteral("按住说话 (按住 V 键说话,松开停止)"), modeGroup);
_toggleRadio = new QRadioButton(QStringLiteral("按键切换 (按 V 键开始/停止说话)"), modeGroup);
_pttRadio->setStyleSheet("color:#e0e0e0;");
_toggleRadio->setStyleSheet("color:#e0e0e0;");
mgLay->addWidget(_pttRadio);
mgLay->addWidget(_toggleRadio);
lay->addWidget(modeGroup);
_statusLabel = new QLabel(this);
_statusLabel->setStyleSheet("color:#5eb3e6;font-size:12px;");
_statusLabel->setAlignment(Qt::AlignCenter);
lay->addWidget(_statusLabel);
auto* btnRow = new QHBoxLayout();
btnRow->addStretch();
auto* applyBtn = new ElaPushButton(QStringLiteral("应用"), this);
applyBtn->setFixedSize(80, 34);
connect(applyBtn, &ElaPushButton::clicked, this, &VoiceSettingsDialog::apply);
auto* closeBtn = new ElaPushButton(QStringLiteral("关闭"), this);
closeBtn->setFixedSize(80, 34);
connect(closeBtn, &ElaPushButton::clicked, this, &QDialog::close);
btnRow->addWidget(applyBtn);
btnRow->addWidget(closeBtn);
lay->addLayout(btnRow);
populate();
}
void VoiceSettingsDialog::populate() {
_inputCombo->clear();
for (const auto& dev : QMediaDevices::audioInputs())
_inputCombo->addItem(dev.description(), QVariant::fromValue(dev));
if (_inputCombo->count() == 0)
_inputCombo->addItem(QStringLiteral("(无可用设备)"));
_outputCombo->clear();
for (const auto& dev : QMediaDevices::audioOutputs())
_outputCombo->addItem(dev.description(), QVariant::fromValue(dev));
if (_outputCombo->count() == 0)
_outputCombo->addItem(QStringLiteral("(无可用设备)"));
auto& v = VoiceManager::instance();
_pttRadio->setChecked(v.mode() == VoiceManager::PushToTalk);
_toggleRadio->setChecked(v.mode() == VoiceManager::Toggle);
_statusLabel->setText(v.isActive() ? QStringLiteral("语音已启动") : QStringLiteral("语音未启动"));
}
void VoiceSettingsDialog::apply() {
auto& v = VoiceManager::instance();
v.setMode(_pttRadio->isChecked() ? VoiceManager::PushToTalk : VoiceManager::Toggle);
auto inDev = _inputCombo->currentData().value<QAudioDevice>();
auto outDev = _outputCombo->currentData().value<QAudioDevice>();
if (!inDev.isNull()) v.setInputDevice(inDev);
if (!outDev.isNull()) v.setOutputDevice(outDev);
if (!v.isActive()) v.start();
_statusLabel->setText(QStringLiteral("已应用,语音已启动"));
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef VOICESETTINGSDIALOG_H
#define VOICESETTINGSDIALOG_H
#include <QDialog>
class QComboBox;
class QRadioButton;
class QLabel;
class VoiceSettingsDialog : public QDialog {
Q_OBJECT
public:
explicit VoiceSettingsDialog(QWidget* parent = nullptr);
private:
void populate();
void apply();
QComboBox* _inputCombo = nullptr;
QComboBox* _outputCombo = nullptr;
QRadioButton* _pttRadio = nullptr;
QRadioButton* _toggleRadio = nullptr;
QLabel* _statusLabel = nullptr;
};
#endif
+51
View File
@@ -0,0 +1,51 @@
#ifndef AUDIORINGBUFFER_H
#define AUDIORINGBUFFER_H
#include <QIODevice>
#include <QByteArray>
#include <QMutex>
class AudioRingBuffer : public QIODevice {
Q_OBJECT
public:
explicit AudioRingBuffer(int capacity = 32000, QObject* parent = nullptr)
: QIODevice(parent), _buf(capacity, 0), _capacity(capacity) { open(ReadWrite); }
void feed(const QByteArray& data) {
QMutexLocker lock(&_mutex);
for (int i = 0; i < data.size(); ++i) {
_buf[_writePos % _capacity] = data[i];
_writePos++;
if (_writePos - _readPos > _capacity)
_readPos = _writePos - _capacity;
}
}
qint64 readData(char* data, qint64 maxlen) override {
QMutexLocker lock(&_mutex);
qint64 avail = _writePos - _readPos;
qint64 toRead = qMin(maxlen, avail);
for (qint64 i = 0; i < toRead; ++i) {
data[i] = _buf[_readPos % _capacity];
_readPos++;
}
if (toRead < maxlen)
memset(data + toRead, 0, maxlen - toRead);
return maxlen;
}
qint64 writeData(const char*, qint64) override { return 0; }
qint64 bytesAvailable() const override {
QMutexLocker lock(&_mutex);
return (_writePos - _readPos) + QIODevice::bytesAvailable();
}
private:
QByteArray _buf;
int _capacity;
qint64 _readPos = 0;
qint64 _writePos = 0;
mutable QMutex _mutex;
};
#endif
+152
View File
@@ -0,0 +1,152 @@
#include "VoiceManager.h"
#include "AudioRingBuffer.h"
#include <QAudioSource>
#include <QAudioSink>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QDateTime>
#include <QTimer>
#include <QtEndian>
#include <cmath>
static const int SAMPLE_RATE = 16000;
static const int FRAME_MS = 20;
static const int FRAME_BYTES = SAMPLE_RATE * 2 * FRAME_MS / 1000;
VoiceManager& VoiceManager::instance() {
static VoiceManager mgr;
return mgr;
}
VoiceManager::VoiceManager(QObject* parent) : QObject(parent) {
_inputDev = QMediaDevices::defaultAudioInput();
_outputDev = QMediaDevices::defaultAudioOutput();
}
QAudioFormat VoiceManager::audioFormat() {
QAudioFormat fmt;
fmt.setSampleRate(SAMPLE_RATE);
fmt.setChannelCount(1);
fmt.setSampleFormat(QAudioFormat::Int16);
return fmt;
}
void VoiceManager::start() {
if (_active) return;
restartCapture();
_active = true;
_muted = true;
emit mutedChanged(true);
}
void VoiceManager::stop() {
if (_capture) {
_capture->stop();
_capture->deleteLater();
_capture = nullptr;
_captureDev = nullptr;
}
for (auto& rs : _remotes) {
if (rs.sink) { rs.sink->stop(); rs.sink->deleteLater(); }
if (rs.buffer) rs.buffer->deleteLater();
}
_remotes.clear();
_active = false;
_muted = true;
}
void VoiceManager::restartCapture() {
if (_capture) {
_capture->stop();
_capture->deleteLater();
_capture = nullptr;
}
if (_inputDev.isNull()) return;
auto fmt = audioFormat();
_capture = new QAudioSource(_inputDev, fmt, this);
_capture->setBufferSize(FRAME_BYTES * 4);
_captureDev = _capture->start();
if (_captureDev)
connect(_captureDev, &QIODevice::readyRead, this, &VoiceManager::onCaptureReady);
}
void VoiceManager::setMuted(bool m) {
if (_muted == m) return;
_muted = m;
emit mutedChanged(m);
}
void VoiceManager::setInputDevice(const QAudioDevice& dev) {
_inputDev = dev;
if (_active) restartCapture();
}
void VoiceManager::setOutputDevice(const QAudioDevice& dev) {
_outputDev = dev;
}
void VoiceManager::keyDown() {
if (!_active) return;
if (_mode == PushToTalk) {
setMuted(false);
} else {
setMuted(!_muted);
}
}
void VoiceManager::keyUp() {
if (!_active) return;
if (_mode == PushToTalk) {
setMuted(true);
}
}
void VoiceManager::onCaptureReady() {
if (!_captureDev) return;
QByteArray data = _captureDev->readAll();
if (data.isEmpty()) return;
double energy = 0;
int samples = data.size() / 2;
const qint16* pcm = reinterpret_cast<const qint16*>(data.constData());
for (int i = 0; i < samples; ++i)
energy += double(pcm[i]) * pcm[i];
energy = (samples > 0) ? std::sqrt(energy / samples) : 0;
bool speaking = energy > 500;
if (speaking != _wasSpeaking) {
_wasSpeaking = speaking;
emit localSpeaking(speaking);
}
if (!_muted && speaking)
emit audioFrame(data);
}
void VoiceManager::processRemoteAudio(const QByteArray& frame) {
if (frame.size() < 3) return;
quint16 idLen = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(frame.constData()));
if (frame.size() < 2 + idLen) return;
QString senderId = QString::fromUtf8(frame.mid(2, idLen));
QByteArray pcm = frame.mid(2 + idLen);
if (!_remotes.contains(senderId)) {
auto fmt = audioFormat();
auto* sink = new QAudioSink(_outputDev, fmt, this);
auto* buf = new AudioRingBuffer(FRAME_BYTES * 50, this);
sink->start(buf);
_remotes[senderId] = {sink, buf, 0};
}
auto& rs = _remotes[senderId];
rs.buffer->feed(pcm);
rs.lastRecv = QDateTime::currentMSecsSinceEpoch();
emit remoteSpeaking(senderId, true);
QTimer::singleShot(300, this, [this, senderId]() {
if (_remotes.contains(senderId)) {
qint64 elapsed = QDateTime::currentMSecsSinceEpoch() - _remotes[senderId].lastRecv;
if (elapsed >= 280) emit remoteSpeaking(senderId, false);
}
});
}
+68
View File
@@ -0,0 +1,68 @@
#ifndef VOICEMANAGER_H
#define VOICEMANAGER_H
#include <QObject>
#include <QAudioFormat>
#include <QAudioDevice>
#include <QMap>
class QAudioSource;
class QAudioSink;
class QIODevice;
class AudioRingBuffer;
class VoiceManager : public QObject {
Q_OBJECT
public:
enum VoiceMode { PushToTalk, Toggle };
static VoiceManager& instance();
void start();
void stop();
void setMuted(bool m);
bool isMuted() const { return _muted; }
bool isActive() const { return _active; }
void setMode(VoiceMode m) { _mode = m; }
VoiceMode mode() const { return _mode; }
void setInputDevice(const QAudioDevice& dev);
void setOutputDevice(const QAudioDevice& dev);
void keyDown();
void keyUp();
void processRemoteAudio(const QByteArray& frame);
static QAudioFormat audioFormat();
signals:
void audioFrame(const QByteArray& pcm);
void localSpeaking(bool speaking);
void remoteSpeaking(const QString& playerId, bool speaking);
void mutedChanged(bool muted);
private:
explicit VoiceManager(QObject* parent = nullptr);
void onCaptureReady();
void restartCapture();
QAudioSource* _capture = nullptr;
QIODevice* _captureDev = nullptr;
QAudioDevice _inputDev;
QAudioDevice _outputDev;
bool _muted = true;
bool _active = false;
bool _wasSpeaking = false;
VoiceMode _mode = PushToTalk;
struct RemoteStream {
QAudioSink* sink = nullptr;
AudioRingBuffer* buffer = nullptr;
qint64 lastRecv = 0;
};
QMap<QString, RemoteStream> _remotes;
};
#endif