756 lines
30 KiB
C++
756 lines
30 KiB
C++
#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);
|
|
}
|