#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 #include "VoiceManager.h" #include #include #include #include #include #include #include #include #include #include #include "VoiceSettingsDialog.h" #include #include #include 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 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 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(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); }