This commit is contained in:
2026-06-18 17:29:28 +08:00
commit 08a070d874
720 changed files with 41946 additions and 0 deletions
+755
View File
@@ -0,0 +1,755 @@
#include "GameWidget.h"
#include "SceneCardItem.h"
#include "CardWidget.h"
#include "PlayerSeatWidget.h"
#include "CardInfoPopup.h"
#include "ChoiceOverlay.h"
#include "SettlementOverlay.h"
#include "CardData.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaMessageBar.h"
#include "NetworkManager.h"
#include <QVBoxLayout>
#include "VoiceManager.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPainter>
#include <QDateTime>
#include <QTimer>
#include <QEvent>
#include <QKeyEvent>
#include <QPropertyAnimation>
#include "VoiceSettingsDialog.h"
#include <QGraphicsPixmapItem>
#include <QGraphicsTextItem>
#include <QOpenGLWidget>
static const QColor BG_MAIN("#1a1f2e");
static const QColor BG_PANEL("#252b3d");
static const QColor ACCENT("#5eb3e6");
static const QColor WARNING("#d94f5c");
static const QColor SUCCESS("#5cb85c");
static const QColor TEXT_PRI("#e0e0e0");
static const QColor TEXT_SEC("#8a8f9d");
static const QColor CARD_BORDER("#8b2635");
GameWidget::GameWidget(QWidget* parent) : QWidget(parent)
{
setStyleSheet(QStringLiteral("background:%1;").arg(BG_MAIN.name()));
setFocusPolicy(Qt::StrongFocus);
initScene();
initFloatingUI();
auto& net = NetworkManager::instance();
connect(&net, &NetworkManager::gameStarted, this, &GameWidget::onGameStart);
connect(&net, &NetworkManager::gameStateSnapshot, this, &GameWidget::onSnapshot);
connect(&net, &NetworkManager::settlementResult, this, &GameWidget::showSettlement);
connect(&net, &NetworkManager::gameEnded, this, [this](const QJsonObject& r) {
addLog(QStringLiteral("游戏结束: ") + r["message"].toString());
ElaMessageBar::information(ElaMessageBarType::Top, QStringLiteral("游戏结束"), r["message"].toString(), 5000, this);
_turnLabel->setText(QStringLiteral("游戏结束"));
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
});
connect(&net, &NetworkManager::errorNotify, this, [this](const QJsonObject& e) {
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("错误"), e["message"].toString(), 3000, this);
});
connect(&net, &NetworkManager::chatMessage, this, [this](const QJsonObject& m) {
auto nick = m["nickname"].toString();
auto text = m["text"].toString();
auto* item = new QListWidgetItem(nick + ": " + text);
item->setForeground(QColor("#e0e0e0"));
_chatList->addItem(item);
_chatList->scrollToBottom();
});
auto& voice = VoiceManager::instance();
connect(&voice, &VoiceManager::audioFrame, this, [](const QByteArray& pcm) {
NetworkManager::instance().sendBinaryFrame(pcm);
});
connect(&net, &NetworkManager::binaryFrameReceived, this, [](const QByteArray& data) {
VoiceManager::instance().processRemoteAudio(data);
});
connect(&voice, &VoiceManager::remoteSpeaking, this, [this](const QString& pid, bool speaking) {
if (_seats.contains(pid))
_seats[pid]->setIsCurrentTurn(speaking);
});
}
void GameWidget::initScene() {
_scene = new QGraphicsScene(this);
_view = new QGraphicsView(_scene, this);
_view->setRenderHint(QPainter::Antialiasing);
_view->setRenderHint(QPainter::SmoothPixmapTransform);
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setFrameShape(QFrame::NoFrame);
_view->setStyleSheet(QStringLiteral("background:%1;").arg(BG_MAIN.name()));
_view->setDragMode(QGraphicsView::NoDrag);
_view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_view);
QPixmap corpsePix(":/images/corpse");
if (!corpsePix.isNull())
_corpseItem = _scene->addPixmap(corpsePix.scaled(140, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation));
_harmonyLabel = _scene->addText("", QFont("Consolas", 12, QFont::Bold));
_harmonyLabel->setDefaultTextColor(TEXT_SEC);
}
void GameWidget::initFloatingUI() {
_topBar = new QWidget(this);
_topBar->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.6);border-bottom:1px solid rgba(94,179,230,0.2);"));
_topBar->setFixedHeight(44);
auto* tbLay = new QHBoxLayout(_topBar);
tbLay->setContentsMargins(16, 0, 16, 0);
_turnLabel = new QLabel(QStringLiteral("等待游戏开始"), _topBar);
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(ACCENT.name()));
_targetLabel = new QLabel(_topBar);
_targetLabel->setStyleSheet(QStringLiteral("color:%1;font-size:16px;font-weight:bold;font-family:Consolas;").arg(QColor("#d4c5a3").name()));
_targetLabel->setAlignment(Qt::AlignCenter);
tbLay->addWidget(_turnLabel);
tbLay->addStretch();
tbLay->addWidget(_targetLabel);
tbLay->addStretch();
auto* voiceStatus = new QLabel(QStringLiteral("🔇"), _topBar);
voiceStatus->setStyleSheet("color:#8a8f9d;font-size:16px;background:transparent;");
voiceStatus->setToolTip(QStringLiteral("V键: 按住说话 / 按下切换"));
tbLay->addWidget(voiceStatus);
auto* voiceSettingsBtn = new ElaPushButton(QStringLiteral(""), _topBar);
voiceSettingsBtn->setFixedSize(30, 30);
voiceSettingsBtn->setToolTip(QStringLiteral("语音设置"));
connect(voiceSettingsBtn, &ElaPushButton::clicked, this, [this]() {
VoiceSettingsDialog dlg(this);
dlg.exec();
});
tbLay->addWidget(voiceSettingsBtn);
connect(&VoiceManager::instance(), &VoiceManager::mutedChanged, voiceStatus, [voiceStatus](bool muted) {
voiceStatus->setText(muted ? QStringLiteral("🔇") : QStringLiteral("🎤"));
voiceStatus->setStyleSheet(muted ? "color:#8a8f9d;font-size:16px;background:transparent;" : "color:#5cb85c;font-size:16px;background:transparent;");
});
_actionBar = new QWidget(this);
_actionBar->setStyleSheet("background:transparent;");
_actionBar->setFixedHeight(50);
auto* abLay = new QHBoxLayout(_actionBar);
abLay->setContentsMargins(0, 4, 0, 4);
abLay->setSpacing(14);
abLay->addStretch();
_skillBtn = new ElaPushButton(QStringLiteral("特技"), _actionBar);
_skillBtn->setFixedSize(100, 38);
_skillBtn->setEnabled(false);
connect(_skillBtn, &ElaPushButton::clicked, this, &GameWidget::onSkill);
_harmonyBtn = new ElaPushButton(QStringLiteral("调和"), _actionBar);
_harmonyBtn->setFixedSize(100, 38);
_harmonyBtn->setEnabled(false);
connect(_harmonyBtn, &ElaPushButton::clicked, this, &GameWidget::onHarmony);
_challengeBtn = new ElaPushButton(QStringLiteral("质疑"), _actionBar);
_challengeBtn->setFixedSize(100, 38);
_challengeBtn->setEnabled(false);
connect(_challengeBtn, &ElaPushButton::clicked, this, &GameWidget::onChallenge);
abLay->addWidget(_skillBtn);
abLay->addWidget(_harmonyBtn);
abLay->addWidget(_challengeBtn);
abLay->addStretch();
_logPanel = new QWidget(this);
_logPanel->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.7);border-left:1px solid rgba(94,179,230,0.15);border-radius:0;"));
auto* logLay = new QVBoxLayout(_logPanel);
logLay->setContentsMargins(8, 8, 8, 8);
logLay->setSpacing(4);
auto* logT = new QLabel(QStringLiteral("游戏日志"), _logPanel);
logT->setStyleSheet(QStringLiteral("color:%1;font-size:12px;font-weight:bold;").arg(TEXT_SEC.name()));
logLay->addWidget(logT);
_logList = new QListWidget(_logPanel);
_logList->setStyleSheet(
QStringLiteral("QListWidget{background:transparent;border:none;color:%1;font-size:12px;font-family:'Consolas';}"
"QListWidget::item{padding:2px 0;}").arg(TEXT_SEC.name()));
logLay->addWidget(_logList, 1);
_cardInfoPopup = new CardInfoPopup(this);
_choiceOverlay = new ChoiceOverlay(this);
_choiceOverlay->hide();
connect(_choiceOverlay, &ChoiceOverlay::responded, this, &GameWidget::onEffectResponse);
_settlementOverlay = new SettlementOverlay(this);
_settlementOverlay->hide();
connect(_settlementOverlay, &SettlementOverlay::finished, this, [this]() { emit gameFinished(); });
_zoomOverlay = new QWidget(this);
_zoomOverlay->hide();
_zoomOverlay->setStyleSheet("background:rgba(0,0,0,180);");
_zoomCard = new CardWidget(_zoomOverlay);
_zoomCard->setFixedSize(270, 390);
_zoomCard->setCardEnabled(false);
_zoomOverlay->installEventFilter(this);
_playAnimWidget = new QWidget(this);
_playAnimWidget->hide();
_playAnimWidget->setStyleSheet("background:rgba(0,0,0,140);");
auto* paLay = new QVBoxLayout(_playAnimWidget);
paLay->setAlignment(Qt::AlignCenter);
paLay->setSpacing(10);
_playAnimCard = new CardWidget(_playAnimWidget);
_playAnimCard->setFixedSize(200, 290);
_playAnimCard->setCardEnabled(false);
paLay->addWidget(_playAnimCard, 0, Qt::AlignCenter);
_playAnimLabel = new QLabel(_playAnimWidget);
_playAnimLabel->setStyleSheet(QStringLiteral("color:%1;font-size:18px;font-weight:bold;background:transparent;").arg(QColor("#d4c5a3").name()));
_playAnimLabel->setAlignment(Qt::AlignCenter);
paLay->addWidget(_playAnimLabel, 0, Qt::AlignCenter);
_chatPanel = new QWidget(this);
_chatPanel->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.7);border-right:1px solid rgba(94,179,230,0.15);"));
auto* chatLay = new QVBoxLayout(_chatPanel);
chatLay->setContentsMargins(8, 8, 8, 8);
chatLay->setSpacing(4);
auto* chatTitle = new QLabel(QStringLiteral("聊天"), _chatPanel);
chatTitle->setStyleSheet(QStringLiteral("color:%1;font-size:12px;font-weight:bold;").arg(TEXT_SEC.name()));
chatLay->addWidget(chatTitle);
_chatList = new QListWidget(_chatPanel);
_chatList->setStyleSheet(QStringLiteral("QListWidget{background:transparent;border:none;color:%1;font-size:12px;}"
"QListWidget::item{padding:2px 0;}").arg(TEXT_PRI.name()));
_chatList->setWordWrap(true);
chatLay->addWidget(_chatList, 1);
auto* chatRow = new QHBoxLayout();
chatRow->setSpacing(4);
_chatInput = new ElaLineEdit(_chatPanel);
_chatInput->setPlaceholderText(QStringLiteral("输入消息..."));
_chatInput->setFixedHeight(28);
connect(_chatInput, &ElaLineEdit::returnPressed, this, &GameWidget::onSendChat);
auto* sendBtn = new ElaPushButton(QStringLiteral("发送"), _chatPanel);
sendBtn->setFixedSize(50, 28);
connect(sendBtn, &ElaPushButton::clicked, this, &GameWidget::onSendChat);
chatRow->addWidget(_chatInput, 1);
chatRow->addWidget(sendBtn);
chatLay->addLayout(chatRow);
_chatToggle = new ElaPushButton(QStringLiteral("💬"), this);
_chatToggle->setFixedSize(36, 36);
_chatToggle->setToolTip(QStringLiteral("聊天"));
connect(_chatToggle, &ElaPushButton::clicked, this, &GameWidget::toggleChat);
_chatPanel->hide();
}
void GameWidget::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
layoutFloatingUI();
layoutPlayerSeats();
QRect ga = gameArea();
int cx = ga.center().x();
_scene->setSceneRect(0, 0, width(), height());
if (_corpseItem) _corpseItem->setPos(cx - 70, ga.y() + ga.height() * 0.08);
if (_harmonyLabel) _harmonyLabel->setPos(cx - 80, ga.y() + ga.height() * 0.25);
qreal cardW = SceneCardItem::W;
int hCount = _harmonyCards.size();
if (hCount > 0) {
qreal hSpacing = qMin(cardW + 10, (ga.width() * 0.6) / qMax(1, hCount));
qreal hTotalW = (hCount - 1) * hSpacing + cardW;
qreal hStartX = cx - hTotalW / 2.0;
qreal hY = ga.y() + ga.height() * 0.32;
for (int i = 0; i < hCount; ++i)
_harmonyCards[i]->setPos(hStartX + i * hSpacing, hY);
}
int handCount = _handCards.size();
if (handCount > 0) {
qreal spacing = qMin(cardW + 10, (ga.width() * 0.8) / qMax(1, handCount));
qreal totalW = (handCount - 1) * spacing + cardW;
qreal startX = cx - totalW / 2.0;
qreal handY = ga.y() + ga.height() * 0.72;
for (int i = 0; i < handCount; ++i) {
_handCards[i]->setPos(startX + i * spacing, handY);
_handCards[i]->setZValue(10 + i);
}
}
}
void GameWidget::layoutFloatingUI() {
int w = width(), h = height();
int chatW = _chatVisible ? 200 : 0;
if (_chatVisible) {
_chatPanel->setGeometry(0, 44, 200, h - 44);
_chatPanel->show();
_chatPanel->raise();
} else {
_chatPanel->hide();
}
_chatToggle->move(chatW + 4, 48);
_chatToggle->raise();
_topBar->setGeometry(0, 0, w, 44);
_topBar->raise();
_actionBar->setGeometry(chatW, h - 54, w - chatW - 220, 50);
_actionBar->raise();
_logPanel->setGeometry(w - 220, 44, 220, h - 44);
_logPanel->raise();
_choiceOverlay->setGeometry(0, 0, w, h);
_settlementOverlay->setGeometry(0, 0, w, h);
_zoomOverlay->setGeometry(0, 0, w, h);
_playAnimWidget->setGeometry(0, 0, w, h);
_zoomCard->move((w - _zoomCard->width()) / 2, (h - _zoomCard->height()) / 2);
}
bool GameWidget::eventFilter(QObject* obj, QEvent* event) {
if (obj == _zoomOverlay && event->type() == QEvent::MouseButtonPress) {
_zoomOverlay->hide();
return true;
}
return QWidget::eventFilter(obj, event);
}
// ---- Game lifecycle ----
void GameWidget::onGameStart(const QJsonObject& config) {
resetGame();
_chatList->clear();
_localPlayerId = config["your_player_id"].toString();
_harmonyTarget = config["harmony_target"].toInt();
_targetLabel->setText(QStringLiteral("目标值: %1").arg(_harmonyTarget));
_turnLabel->setText(QStringLiteral("游戏开始!"));
_harmonyLabel->setPlainText(QStringLiteral("调和区 (目标: %1)").arg(_harmonyTarget));
addLog(QStringLiteral("游戏开始 - 调和目标值 %1").arg(_harmonyTarget));
}
void GameWidget::onSnapshot(const QJsonObject& snap) {
_localPlayerId = snap["your_player_id"].toString();
auto phase = snap["phase"].toString();
auto turnPhase = snap["turn_phase"].toString();
auto turnPlayer = snap["current_turn"].toString();
auto players = snap["players"].toArray();
_playerOrder.clear();
for (const auto& pv : players)
_playerOrder.append(pv.toObject()["player_id"].toString());
for (const auto& pv : players) {
auto po = pv.toObject();
auto pid = po["player_id"].toString();
if (pid == _localPlayerId) continue;
if (!_seats.contains(pid)) {
auto* seat = new PlayerSeatWidget(this);
connect(seat, &PlayerSeatWidget::playerClicked, this, &GameWidget::onPlayerSeatClicked);
_seats[pid] = seat;
}
_seats[pid]->setPlayerData(po);
_seats[pid]->setIsCurrentTurn(pid == turnPlayer);
}
QStringList gone;
for (auto it = _seats.begin(); it != _seats.end(); ++it) {
bool found = false;
for (const auto& pv : players)
if (pv.toObject()["player_id"].toString() == it.key() && it.key() != _localPlayerId)
found = true;
if (!found) gone.append(it.key());
}
for (const auto& k : gone) { _seats[k]->deleteLater(); _seats.remove(k); }
layoutPlayerSeats();
updateHandCards(snap["hand_cards"].toArray());
updateHarmonyZone(snap["harmony_zone"].toArray());
if (snap.contains("effect")) {
auto eInfo = snap["effect"].toObject();
if (eInfo["needs_response"].toBool()) {
auto ct = eInfo["choice_type"].toString();
if (ct == "all_select_card" || ct == "select_card_from_hand") {
auto cd = eInfo["choice_data"].toObject();
if (!cd.contains("cards")) { cd["cards"] = snap["hand_cards"]; eInfo["choice_data"] = cd; }
}
}
handleEffectState(eInfo);
return;
}
_choiceOverlay->hideChoice();
bool myTurn = (turnPlayer == _localPlayerId);
if (phase == "all_exited") {
_turnLabel->setText(QStringLiteral("所有人已退出 — 等待结算"));
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
return;
}
if (!myTurn) {
QString nick;
for (const auto& pv : players)
if (pv.toObject()["player_id"].toString() == turnPlayer)
nick = pv.toObject()["nickname"].toString();
_turnLabel->setText(QStringLiteral("等待 %1 行动...").arg(nick));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;").arg(TEXT_SEC.name()));
_phase = Phase::Idle;
setActionsEnabled(false, false, false);
return;
}
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(ACCENT.name()));
if (turnPhase == "select_card") {
_turnLabel->setText(QStringLiteral("你的回合 — 选择手牌"));
_phase = Phase::SelectCard;
for (auto* c : _handCards) {
const CardDef* def = CardDatabase::instance().getCardDef(c->typeId());
c->setCardEnabled(!(def && def->unusable));
}
setActionsEnabled(false, false, false);
} else if (turnPhase == "select_action") {
_turnLabel->setText(QStringLiteral("选择行动方式"));
_phase = Phase::SelectAction;
} else if (turnPhase == "select_target") {
_turnLabel->setText(QStringLiteral("选择质疑目标"));
_phase = Phase::SelectTarget;
}
for (const auto& pv : players) {
if (pv.toObject()["player_id"].toString() == _localPlayerId && pv.toObject()["is_exited"].toBool()) {
_turnLabel->setText(QStringLiteral("你已暂时退出"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;").arg(TEXT_SEC.name()));
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
}
}
}
void GameWidget::resetGame() {
VoiceManager::instance().stop();
for (auto* c : _handCards) { _scene->removeItem(c); delete c; }
_handCards.clear();
for (auto* c : _harmonyCards) { _scene->removeItem(c); delete c; }
_harmonyCards.clear();
for (auto* s : _seats) s->deleteLater();
_seats.clear();
_playerOrder.clear();
_selectedCardUid.clear();
_phase = Phase::Idle;
_logList->clear();
setActionsEnabled(false, false, false);
_turnLabel->setText(QStringLiteral("等待游戏开始"));
_targetLabel->clear();
_harmonyLabel->setPlainText("");
_choiceOverlay->hideChoice();
}
// ---- Scene layout ----
void GameWidget::layoutPlayerSeats() {
QVector<QString> others;
for (const auto& pid : _playerOrder)
if (pid != _localPlayerId && _seats.contains(pid))
others.append(pid);
int n = others.size();
if (n == 0) return;
QRect ga = gameArea();
int gx = ga.x(), gy = ga.y(), gw = ga.width(), gh = ga.height();
int pw = 180, ph = 130;
struct Pos { int x, y; };
QVector<Pos> positions;
if (n == 1) {
positions = {{gx + gw / 2 - pw / 2, gy}};
} else if (n == 2) {
positions = {{gx + gw / 4 - pw / 2, gy}, {gx + 3 * gw / 4 - pw / 2, gy}};
} else if (n == 3) {
positions = {{gx, gy + gh / 2 - ph / 2}, {gx + gw / 2 - pw / 2, gy}, {gx + gw - pw, gy + gh / 2 - ph / 2}};
} else if (n == 4) {
positions = {{gx, gy + gh / 2 - ph / 2}, {gx + gw / 3 - pw / 2, gy}, {gx + 2 * gw / 3 - pw / 2, gy}, {gx + gw - pw, gy + gh / 2 - ph / 2}};
} else {
positions.append({gx, gy + gh / 2 - ph / 2});
int topCount = n - 2;
for (int i = 0; i < topCount; ++i)
positions.append({gx + int((i + 1) * double(gw) / (topCount + 1) - pw / 2), gy});
positions.append({gx + gw - pw, gy + gh / 2 - ph / 2});
}
for (int i = 0; i < n && i < positions.size(); ++i) {
_seats[others[i]]->setGeometry(positions[i].x, positions[i].y, pw, ph);
_seats[others[i]]->show();
_seats[others[i]]->raise();
}
}
void GameWidget::updateHandCards(const QJsonArray& cards) {
for (auto* c : _handCards) { _scene->removeItem(c); delete c; }
_handCards.clear();
_selectedCardUid.clear();
setActionsEnabled(false, false, false);
QRect ga = gameArea();
int cx = ga.center().x();
int count = cards.size();
qreal cardW = SceneCardItem::W;
qreal spacing = qMin(cardW + 10, (ga.width() * 0.8) / qMax(1, count));
qreal totalW = (count - 1) * spacing + cardW;
qreal startX = cx - totalW / 2.0;
qreal handY = ga.y() + ga.height() * 0.72;
for (int i = 0; i < count; ++i) {
auto c = cards[i].toObject();
auto* item = new SceneCardItem();
item->setCardData(c["uid"].toString(), c["type_id"].toString(), true);
item->setPos(startX + i * spacing, handY);
item->setZValue(10 + i);
const CardDef* def = CardDatabase::instance().getCardDef(c["type_id"].toString());
item->setCardEnabled(_phase == Phase::SelectCard && !(def && def->unusable));
connect(item, &SceneCardItem::clicked, this, &GameWidget::onSceneCardClicked);
connect(item, &SceneCardItem::hoverIn, this, &GameWidget::onSceneCardHover);
connect(item, &SceneCardItem::hoverOut, this, &GameWidget::onSceneCardHoverOut);
connect(item, &SceneCardItem::rightClicked, this, &GameWidget::showCardZoom);
_scene->addItem(item);
_handCards.append(item);
}
}
void GameWidget::updateHarmonyZone(const QJsonArray& cards) {
for (auto* c : _harmonyCards) { _scene->removeItem(c); delete c; }
_harmonyCards.clear();
_harmonyLabel->setPlainText(QStringLiteral("调和区 (%1张 / 目标: %2)").arg(cards.size()).arg(_harmonyTarget));
QRect ga = gameArea();
int cx = ga.center().x();
int count = cards.size();
qreal cardW = SceneCardItem::W;
qreal spacing = qMin(cardW + 10, (ga.width() * 0.6) / qMax(1, count));
qreal totalW = (count - 1) * spacing + cardW;
qreal startX = cx - totalW / 2.0;
qreal hY = ga.y() + ga.height() * 0.32;
for (int i = 0; i < count; ++i) {
auto c = cards[i].toObject();
auto* item = new SceneCardItem();
item->setCardData(c["uid"].toString(), c.contains("type_id") ? c["type_id"].toString() : "", c["face_up"].toBool(false));
item->setPos(startX + i * spacing, hY);
item->setScale(0.8);
item->setCardEnabled(false);
item->setZValue(5);
_scene->addItem(item);
_harmonyCards.append(item);
}
}
// ---- Interaction handlers ----
void GameWidget::onSceneCardClicked(const QString& uid) {
if (_phase != Phase::SelectCard) return;
bool wasSelected = false;
for (auto* c : _handCards) {
if (c->uid() == uid) {
wasSelected = c->isCardSelected();
c->setCardSelected(!wasSelected);
} else {
c->setCardSelected(false);
}
}
if (wasSelected) { _selectedCardUid.clear(); setActionsEnabled(false, false, false); }
else { _selectedCardUid = uid; setActionsEnabled(true, true, true); }
}
void GameWidget::onSceneCardHover(const QString& typeId) {
auto* item = qobject_cast<SceneCardItem*>(sender());
if (item) {
QPointF scenePos = item->mapToScene(SceneCardItem::W, 0);
QPoint viewPos = _view->mapFromScene(scenePos);
QPoint globalPos = _view->viewport()->mapToGlobal(viewPos);
_cardInfoPopup->showForCard(typeId, globalPos);
}
}
void GameWidget::onSceneCardHoverOut() {
_cardInfoPopup->hidePopup();
}
void GameWidget::onSkill() {
if (_selectedCardUid.isEmpty()) return;
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
const CardDef* def = CardDatabase::instance().getCardDef(typeId);
showPlayAnimation(typeId, QStringLiteral("特技: ") + (def ? def->name : typeId));
NetworkManager::instance().selectCard(_selectedCardUid);
NetworkManager::instance().confirmAction("skill");
_phase = Phase::WaitResponse;
setActionsEnabled(false, false, false);
addLog(QStringLiteral("使用特技: ") + (def ? def->name : typeId));
}
void GameWidget::onHarmony() {
if (_selectedCardUid.isEmpty()) return;
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
showPlayAnimation(typeId, QStringLiteral("调和"));
NetworkManager::instance().selectCard(_selectedCardUid);
NetworkManager::instance().confirmAction("harmony");
_phase = Phase::WaitResponse;
setActionsEnabled(false, false, false);
addLog(QStringLiteral("放入调和区"));
}
void GameWidget::onChallenge() {
if (_selectedCardUid.isEmpty()) return;
_phase = Phase::SelectTarget;
_turnLabel->setText(QStringLiteral("点击目标玩家"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(WARNING.name()));
setActionsEnabled(false, false, false);
for (auto* s : _seats) s->setCursor(Qt::PointingHandCursor);
addLog(QStringLiteral("选择质疑目标..."));
}
void GameWidget::onPlayerSeatClicked(const QString& playerId) {
if (_phase != Phase::SelectTarget || _selectedCardUid.isEmpty()) return;
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
showPlayAnimation(typeId, QStringLiteral("质疑"));
NetworkManager::instance().selectCard(_selectedCardUid);
QJsonObject extra;
extra["target_player_id"] = playerId;
NetworkManager::instance().confirmAction("challenge", extra);
_phase = Phase::WaitResponse;
for (auto* s : _seats) s->setCursor(Qt::ArrowCursor);
addLog(QStringLiteral("质疑 ") + playerId);
}
// ---- Effect / Settlement / Overlays ----
void GameWidget::handleEffectState(const QJsonObject& info) {
if (info["needs_response"].toBool()) {
auto ct = info["choice_type"].toString();
auto cd = info["choice_data"].toObject();
addLog(QStringLiteral("效果: ") + info["card_type"].toString() + " - " + cd["message"].toString());
_choiceOverlay->showChoice(ct, cd);
_turnLabel->setText(QStringLiteral("效果结算 — 请做出选择"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(QColor("#d4c5a3").name()));
} else {
_turnLabel->setText(info["waiting_message"].toString());
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;").arg(TEXT_SEC.name()));
addLog(QStringLiteral("效果结算中: ") + info["card_type"].toString());
}
}
void GameWidget::onEffectResponse(const QJsonObject& resp) {
NetworkManager::instance().effectResponse(resp);
_turnLabel->setText(QStringLiteral("等待效果结算..."));
}
void GameWidget::showSettlement(const QJsonObject& data) {
_phase = Phase::Watching;
setActionsEnabled(false, false, false);
_turnLabel->setText(QStringLiteral("结算中..."));
addLog(QStringLiteral("进入结算阶段"));
_settlementOverlay->showSettlement(data);
}
void GameWidget::showCardZoom(const QString& typeId) {
if (typeId.isEmpty()) return;
_zoomCard->setCardType(typeId);
_zoomCard->setFaceUp(true);
_zoomCard->rebuildCache();
_zoomOverlay->setGeometry(rect());
_zoomCard->move((width() - _zoomCard->width()) / 2, (height() - _zoomCard->height()) / 2);
_zoomOverlay->show();
_zoomOverlay->raise();
}
void GameWidget::showPlayAnimation(const QString& typeId, const QString& actionName) {
_playAnimCard->setCardType(typeId);
_playAnimCard->setFaceUp(true);
_playAnimCard->rebuildCache();
_playAnimLabel->setText(actionName);
_playAnimWidget->setGeometry(rect());
_playAnimWidget->show();
_playAnimWidget->raise();
QTimer::singleShot(1200, this, [this]() { _playAnimWidget->hide(); });
}
void GameWidget::animateCardToPos(SceneCardItem* card, const QPointF& target, int duration) {
auto* anim = new QPropertyAnimation(card, "pos", this);
anim->setDuration(duration);
anim->setStartValue(card->pos());
anim->setEndValue(target);
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->start(QAbstractAnimation::DeleteWhenStopped);
}
// ---- Helpers ----
void GameWidget::setActionsEnabled(bool s, bool h, bool c) {
_skillBtn->setEnabled(s);
_harmonyBtn->setEnabled(h);
_challengeBtn->setEnabled(c);
}
void GameWidget::addLog(const QString& text) {
auto* item = new QListWidgetItem("[" + QDateTime::currentDateTime().toString("hh:mm:ss") + "] " + text);
item->setForeground(text.contains(QStringLiteral("效果")) ? ACCENT : TEXT_SEC);
_logList->addItem(item);
_logList->scrollToBottom();
}
QRect GameWidget::gameArea() const {
int chatW = _chatVisible ? 200 : 0;
return {chatW, 44, width() - chatW - 220, height() - 44 - 54};
}
void GameWidget::onSendChat() {
auto text = _chatInput->text().trimmed();
if (text.isEmpty()) return;
NetworkManager::instance().sendChat(text);
_chatInput->clear();
}
void GameWidget::toggleChat() {
_chatVisible = !_chatVisible;
layoutFloatingUI();
layoutPlayerSeats();
QRect ga = gameArea();
int cx = ga.center().x();
_scene->setSceneRect(0, 0, width(), height());
if (_corpseItem) _corpseItem->setPos(cx - 70, ga.y() + ga.height() * 0.08);
if (_harmonyLabel) _harmonyLabel->setPos(cx - 80, ga.y() + ga.height() * 0.25);
qreal cardW = SceneCardItem::W;
int hc = _harmonyCards.size();
if (hc > 0) {
qreal sp = qMin(cardW + 10, (ga.width() * 0.6) / qMax(1, hc));
qreal tw = (hc - 1) * sp + cardW;
for (int i = 0; i < hc; ++i) _harmonyCards[i]->setPos(cx - tw / 2.0 + i * sp, ga.y() + ga.height() * 0.32);
}
int nc = _handCards.size();
if (nc > 0) {
qreal sp = qMin(cardW + 10, (ga.width() * 0.8) / qMax(1, nc));
qreal tw = (nc - 1) * sp + cardW;
for (int i = 0; i < nc; ++i) { _handCards[i]->setPos(cx - tw / 2.0 + i * sp, ga.y() + ga.height() * 0.72); _handCards[i]->setZValue(10 + i); }
}
}
void GameWidget::keyPressEvent(QKeyEvent* e) {
if (e->key() == Qt::Key_V && !e->isAutoRepeat()) {
if (_chatInput && _chatInput->hasFocus()) { QWidget::keyPressEvent(e); return; }
VoiceManager::instance().keyDown();
return;
}
QWidget::keyPressEvent(e);
}
void GameWidget::keyReleaseEvent(QKeyEvent* e) {
if (e->key() == Qt::Key_V && !e->isAutoRepeat()) {
if (_chatInput && _chatInput->hasFocus()) { QWidget::keyReleaseEvent(e); return; }
VoiceManager::instance().keyUp();
return;
}
QWidget::keyReleaseEvent(e);
}