frist
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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("已应用,语音已启动"));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user