增加和修改了非常多的功能

This commit is contained in:
2026-06-19 22:09:18 +08:00
parent 08a070d874
commit 156c172c6a
54 changed files with 1289 additions and 149 deletions
+96
View File
@@ -0,0 +1,96 @@
#include "SettingsManager.h"
#include <QFile>
#include <QDir>
#include <QJsonDocument>
#include <QCoreApplication>
SettingsManager& SettingsManager::instance() {
static SettingsManager mgr;
return mgr;
}
SettingsManager::SettingsManager(QObject* parent) : QObject(parent) {}
QString SettingsManager::filePath() const {
return QCoreApplication::applicationDirPath() + "/config/settings.json";
}
void SettingsManager::load() {
QFile f(filePath());
if (!f.exists() || !f.open(QIODevice::ReadOnly)) {
_data = defaultSettings();
save();
return;
}
auto doc = QJsonDocument::fromJson(f.readAll());
f.close();
if (doc.isObject()) {
_data = doc.object();
} else {
_data = defaultSettings();
save();
}
}
void SettingsManager::save() {
QDir().mkpath(QFileInfo(filePath()).absolutePath());
QFile f(filePath());
if (f.open(QIODevice::WriteOnly)) {
f.write(QJsonDocument(_data).toJson(QJsonDocument::Indented));
f.close();
}
}
void SettingsManager::resetToDefaults() {
_data = defaultSettings();
emit settingsChanged();
}
QJsonObject SettingsManager::defaultSettings() const {
return {
{"general", QJsonObject{
{"nickname", "Player"},
}},
{"network", QJsonObject{
{"server_address", "ws://127.0.0.1:8080/ws"},
}},
{"voice", QJsonObject{
{"mode", "ptt"},
{"ptt_key", "V"},
{"input_device", ""},
{"output_device", ""},
}},
{"audio", QJsonObject{
{"master_volume", 80},
{"voice_volume", 100},
}},
{"display", QJsonObject{
{"animations_enabled", true},
{"animation_speed", 1.0},
{"theme", "dark"},
}},
};
}
QString SettingsManager::getString(const QString& sec, const QString& key, const QString& def) const {
return _data[sec].toObject()[key].toString(def);
}
int SettingsManager::getInt(const QString& sec, const QString& key, int def) const {
return _data[sec].toObject()[key].toInt(def);
}
bool SettingsManager::getBool(const QString& sec, const QString& key, bool def) const {
auto v = _data[sec].toObject()[key];
return v.isUndefined() ? def : v.toBool(def);
}
double SettingsManager::getDouble(const QString& sec, const QString& key, double def) const {
return _data[sec].toObject()[key].toDouble(def);
}
void SettingsManager::set(const QString& sec, const QString& key, const QJsonValue& val) {
auto obj = _data[sec].toObject();
obj[key] = val;
_data[sec] = obj;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef SETTINGSMANAGER_H
#define SETTINGSMANAGER_H
#include <QObject>
#include <QJsonObject>
#include <QString>
class SettingsManager : public QObject {
Q_OBJECT
public:
static SettingsManager& instance();
void load();
void save();
void resetToDefaults();
QString getString(const QString& section, const QString& key, const QString& def = {}) const;
int getInt(const QString& section, const QString& key, int def = 0) const;
bool getBool(const QString& section, const QString& key, bool def = false) const;
double getDouble(const QString& section, const QString& key, double def = 0) const;
void set(const QString& section, const QString& key, const QJsonValue& val);
QString filePath() const;
signals:
void settingsChanged();
private:
explicit SettingsManager(QObject* parent = nullptr);
QJsonObject defaultSettings() const;
QJsonObject _data;
};
#endif
+83
View File
@@ -0,0 +1,83 @@
#include "SoundManager.h"
#include "SettingsManager.h"
#include <QCoreApplication>
#include <QFileInfo>
#include <QDir>
#include <QDirIterator>
#include <QUrl>
#include <QDebug>
SoundManager& SoundManager::instance() {
static SoundManager mgr;
return mgr;
}
SoundManager::SoundManager(QObject* parent) : QObject(parent) {}
void SoundManager::init() {
_volume = SettingsManager::instance().getInt("audio", "master_volume", 80) / 100.0;
QStringList searchPaths = {
QCoreApplication::applicationDirPath() + "/Res/Sound/",
QCoreApplication::applicationDirPath() + "/../Res/Sound/",
QDir::currentPath() + "/Res/Sound/",
};
_basePath.clear();
for (const auto& p : searchPaths) {
if (QDir(p).exists()) {
_basePath = QDir(p).absolutePath() + "/";
break;
}
}
if (_basePath.isEmpty()) {
qWarning() << "[SoundManager] Sound directory not found. Searched:" << searchPaths;
return;
}
qDebug() << "[SoundManager] Sound directory:" << _basePath;
QDirIterator it(_basePath, {"*.wav"}, QDir::Files, QDirIterator::Subdirectories);
int loaded = 0;
while (it.hasNext()) {
it.next();
QString name = it.fileInfo().baseName();
auto* sfx = new QSoundEffect(this);
sfx->setSource(QUrl::fromLocalFile(it.filePath()));
sfx->setVolume(_volume);
_sounds[name] = sfx;
loaded++;
}
qDebug() << "[SoundManager] Loaded" << loaded << "sounds";
connect(&SettingsManager::instance(), &SettingsManager::settingsChanged, this, [this]() {
_volume = SettingsManager::instance().getInt("audio", "master_volume", 80) / 100.0;
});
}
bool SoundManager::preload(const QString& name) {
QString path = _basePath + name + ".wav";
if (!QFileInfo::exists(path)) {
qDebug() << "[SoundManager] Missing:" << name << ".wav";
return false;
}
auto* sfx = new QSoundEffect(this);
sfx->setSource(QUrl::fromLocalFile(path));
sfx->setVolume(_volume);
_sounds[name] = sfx;
return true;
}
void SoundManager::play(const QString& name) {
auto it = _sounds.find(name);
if (it == _sounds.end()) return;
auto* sfx = *it;
sfx->setVolume(_volume);
if (sfx->isLoaded()) {
sfx->play();
}
}
void SoundManager::setMasterVolume(qreal v) {
_volume = v;
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef SOUNDMANAGER_H
#define SOUNDMANAGER_H
#include <QObject>
#include <QMap>
#include <QSoundEffect>
class SoundManager : public QObject {
Q_OBJECT
public:
static SoundManager& instance();
void init();
void play(const QString& name);
void setMasterVolume(qreal v);
qreal masterVolume() const { return _volume; }
private:
explicit SoundManager(QObject* parent = nullptr);
bool preload(const QString& name);
QMap<QString, QSoundEffect*> _sounds;
qreal _volume = 0.8;
QString _basePath;
};
#endif
+6
View File
@@ -165,5 +165,11 @@ void NetworkManager::handleServerMessage(const QJsonObject& msg) {
emit roomListReceived(payload["rooms"].toArray());
} else if (type == "settlement_result") {
emit settlementResult(payload);
} else if (type == "game_log") {
emit gameLogReceived(payload["text"].toString());
} else if (type == "login_result") {
emit loginResult(payload);
} else if (type == "register_result") {
emit registerResult(payload);
}
}
+3
View File
@@ -58,6 +58,9 @@ signals:
void roomListReceived(const QJsonArray& rooms);
void settlementResult(const QJsonObject& data);
void binaryFrameReceived(const QByteArray& data);
void gameLogReceived(const QString& text);
void loginResult(const QJsonObject& result);
void registerResult(const QJsonObject& result);
private slots:
void onConnected();
+1 -1
View File
@@ -120,7 +120,7 @@ void CardWidget::paintEvent(QPaintEvent*) {
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.setFont(QFont(QString(), 9, QFont::Bold));
p.setPen(def->mp >= 0 ? QColor("#ffd700") : QColor("#ff4444"));
p.drawText(badge, Qt::AlignCenter, QString::number(def->mp));
}
+3 -3
View File
@@ -51,10 +51,10 @@ void ChoiceOverlay::showChoice(const QString& choiceType, const QJsonObject& dat
else if (choiceType == "select_option")
buildSelectOption(data);
_panel->setMaximumHeight(height() - 40);
_panel->adjustSize();
int panelH = qBound(280, height() * 2 / 3, 520);
_panel->setFixedSize(660, panelH);
show(); raise();
_panel->move((width() - _panel->width()) / 2, qMax(20, (height() - _panel->height()) / 2));
_panel->move((width() - 660) / 2, qMax(20, (height() - panelH) / 2));
}
void ChoiceOverlay::hideChoice() { clearContent(); hide(); }
+98 -16
View File
@@ -22,8 +22,14 @@
#include <QTimer>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QFileDialog>
#include <QTextStream>
#include <QMessageBox>
#include <QPropertyAnimation>
#include "VoiceSettingsDialog.h"
#include "SettingsDialog.h"
#include "SoundManager.h"
#include "SpectrumWidget.h"
#include <QGraphicsPixmapItem>
#include <QGraphicsTextItem>
#include <QOpenGLWidget>
@@ -41,6 +47,7 @@ GameWidget::GameWidget(QWidget* parent) : QWidget(parent)
{
setStyleSheet(QStringLiteral("background:%1;").arg(BG_MAIN.name()));
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
initScene();
initFloatingUI();
@@ -58,6 +65,9 @@ GameWidget::GameWidget(QWidget* parent) : QWidget(parent)
connect(&net, &NetworkManager::errorNotify, this, [this](const QJsonObject& e) {
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("错误"), e["message"].toString(), 3000, this);
});
connect(&net, &NetworkManager::gameLogReceived, this, [this](const QString& text) {
addLog(text);
});
connect(&net, &NetworkManager::chatMessage, this, [this](const QJsonObject& m) {
auto nick = m["nickname"].toString();
auto text = m["text"].toString();
@@ -65,6 +75,7 @@ GameWidget::GameWidget(QWidget* parent) : QWidget(parent)
item->setForeground(QColor("#e0e0e0"));
_chatList->addItem(item);
_chatList->scrollToBottom();
SoundManager::instance().play("chat_message");
});
auto& voice = VoiceManager::instance();
@@ -100,7 +111,7 @@ void GameWidget::initScene() {
if (!corpsePix.isNull())
_corpseItem = _scene->addPixmap(corpsePix.scaled(140, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation));
_harmonyLabel = _scene->addText("", QFont("Consolas", 12, QFont::Bold));
_harmonyLabel = _scene->addText("", QFont("monospace", 12, QFont::Bold));
_harmonyLabel->setDefaultTextColor(TEXT_SEC);
}
@@ -113,7 +124,7 @@ void GameWidget::initFloatingUI() {
_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->setStyleSheet(QStringLiteral("color:%1;font-size:16px;font-weight:bold;font-family:monospace;").arg(QColor("#d4c5a3").name()));
_targetLabel->setAlignment(Qt::AlignCenter);
tbLay->addWidget(_turnLabel);
tbLay->addStretch();
@@ -123,14 +134,28 @@ void GameWidget::initFloatingUI() {
voiceStatus->setStyleSheet("color:#8a8f9d;font-size:16px;background:transparent;");
voiceStatus->setToolTip(QStringLiteral("V键: 按住说话 / 按下切换"));
tbLay->addWidget(voiceStatus);
auto* spectrum = new SpectrumWidget(12, _topBar);
tbLay->addWidget(spectrum);
connect(&VoiceManager::instance(), &VoiceManager::spectrumData, spectrum, &SpectrumWidget::updateLevels);
auto* voiceSettingsBtn = new ElaPushButton(QStringLiteral(""), _topBar);
voiceSettingsBtn->setFixedSize(30, 30);
voiceSettingsBtn->setToolTip(QStringLiteral("语音设置"));
connect(voiceSettingsBtn, &ElaPushButton::clicked, this, [this]() {
VoiceSettingsDialog dlg(this);
SettingsDialog dlg(this);
dlg.exec();
});
tbLay->addWidget(voiceSettingsBtn);
auto* exitBtn = new ElaPushButton(QStringLiteral("退出"), _topBar);
exitBtn->setFixedSize(50, 28);
connect(exitBtn, &ElaPushButton::clicked, this, [this]() {
auto r = QMessageBox::question(this, QStringLiteral("退出对局"),
QStringLiteral("确定要退出当前对局吗?"), QMessageBox::Yes | QMessageBox::No);
if (r == QMessageBox::Yes) {
NetworkManager::instance().leaveRoom();
emit gameFinished();
}
});
tbLay->addWidget(exitBtn);
connect(&VoiceManager::instance(), &VoiceManager::mutedChanged, voiceStatus, [voiceStatus](bool muted) {
voiceStatus->setText(muted ? QStringLiteral("🔇") : QStringLiteral("🎤"));
@@ -168,10 +193,26 @@ void GameWidget::initFloatingUI() {
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);
auto* logExportBtn = new ElaPushButton(QStringLiteral("导出"), _logPanel);
logExportBtn->setFixedSize(44, 22);
connect(logExportBtn, &ElaPushButton::clicked, this, [this]() {
QString path = QFileDialog::getSaveFileName(this, QStringLiteral("导出游戏日志"), QStringLiteral("game_log.txt"), QStringLiteral("文本文件 (*.txt)"));
if (path.isEmpty()) return;
QFile f(path);
if (f.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream ts(&f);
for (int i = 0; i < _logList->count(); ++i) ts << _logList->item(i)->text() << "\n";
f.close();
}
});
auto* logHeader = new QHBoxLayout();
logHeader->addWidget(logT);
logHeader->addStretch();
logHeader->addWidget(logExportBtn);
logLay->addLayout(logHeader);
_logList = new QListWidget(_logPanel);
_logList->setStyleSheet(
QStringLiteral("QListWidget{background:transparent;border:none;color:%1;font-size:12px;font-family:'Consolas';}"
QStringLiteral("QListWidget{background:transparent;border:none;color:%1;font-size:12px;font-family:monospace;}"
"QListWidget::item{padding:2px 0;}").arg(TEXT_SEC.name()));
logLay->addWidget(_logList, 1);
@@ -294,9 +335,9 @@ void GameWidget::layoutFloatingUI() {
_topBar->setGeometry(0, 0, w, 44);
_topBar->raise();
_actionBar->setGeometry(chatW, h - 54, w - chatW - 220, 50);
_actionBar->setGeometry(chatW, h - 54, w - chatW - _logWidth, 50);
_actionBar->raise();
_logPanel->setGeometry(w - 220, 44, 220, h - 44);
_logPanel->setGeometry(w - _logWidth, 44, _logWidth, h - 44);
_logPanel->raise();
_choiceOverlay->setGeometry(0, 0, w, h);
_settlementOverlay->setGeometry(0, 0, w, h);
@@ -318,6 +359,7 @@ bool GameWidget::eventFilter(QObject* obj, QEvent* event) {
void GameWidget::onGameStart(const QJsonObject& config) {
resetGame();
_chatList->clear();
SoundManager::instance().play("game_start");
_localPlayerId = config["your_player_id"].toString();
_harmonyTarget = config["harmony_target"].toInt();
_targetLabel->setText(QStringLiteral("目标值: %1").arg(_harmonyTarget));
@@ -399,6 +441,7 @@ void GameWidget::onSnapshot(const QJsonObject& snap) {
if (turnPhase == "select_card") {
_turnLabel->setText(QStringLiteral("你的回合 — 选择手牌"));
_phase = Phase::SelectCard;
SoundManager::instance().play("turn_start");
for (auto* c : _handCards) {
const CardDef* def = CardDatabase::instance().getCardDef(c->typeId());
c->setCardEnabled(!(def && def->unusable));
@@ -546,6 +589,7 @@ void GameWidget::updateHarmonyZone(const QJsonArray& cards) {
void GameWidget::onSceneCardClicked(const QString& uid) {
if (_phase != Phase::SelectCard) return;
SoundManager::instance().play("card_select");
bool wasSelected = false;
for (auto* c : _handCards) {
if (c->uid() == uid) {
@@ -579,11 +623,10 @@ void GameWidget::onSkill() {
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));
SoundManager::instance().play("action_skill");
NetworkManager::instance().selectCard(_selectedCardUid);
NetworkManager::instance().confirmAction("skill");
_phase = Phase::WaitResponse;
setActionsEnabled(false, false, false);
addLog(QStringLiteral("使用特技: ") + (def ? def->name : typeId));
_phase = Phase::WaitResponse; setActionsEnabled(false,false,false);
}
void GameWidget::onHarmony() {
@@ -591,11 +634,10 @@ void GameWidget::onHarmony() {
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
showPlayAnimation(typeId, QStringLiteral("调和"));
SoundManager::instance().play("action_harmony");
NetworkManager::instance().selectCard(_selectedCardUid);
NetworkManager::instance().confirmAction("harmony");
_phase = Phase::WaitResponse;
setActionsEnabled(false, false, false);
addLog(QStringLiteral("放入调和区"));
_phase = Phase::WaitResponse; setActionsEnabled(false,false,false);
}
void GameWidget::onChallenge() {
@@ -613,13 +655,13 @@ void GameWidget::onPlayerSeatClicked(const QString& playerId) {
QString typeId;
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
showPlayAnimation(typeId, QStringLiteral("质疑"));
SoundManager::instance().play("action_challenge");
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 ----
@@ -629,6 +671,7 @@ void GameWidget::handleEffectState(const QJsonObject& info) {
auto ct = info["choice_type"].toString();
auto cd = info["choice_data"].toObject();
addLog(QStringLiteral("效果: ") + info["card_type"].toString() + " - " + cd["message"].toString());
SoundManager::instance().play("effect_trigger");
_choiceOverlay->showChoice(ct, cd);
_turnLabel->setText(QStringLiteral("效果结算 — 请做出选择"));
_turnLabel->setStyleSheet(QStringLiteral("color:%1;font-size:14px;font-weight:bold;").arg(QColor("#d4c5a3").name()));
@@ -700,7 +743,7 @@ void GameWidget::addLog(const QString& text) {
QRect GameWidget::gameArea() const {
int chatW = _chatVisible ? 200 : 0;
return {chatW, 44, width() - chatW - 220, height() - 44 - 54};
return {chatW, 44, width() - chatW - _logWidth, height() - 44 - 54};
}
void GameWidget::onSendChat() {
@@ -753,3 +796,42 @@ void GameWidget::keyReleaseEvent(QKeyEvent* e) {
}
QWidget::keyReleaseEvent(e);
}
void GameWidget::mousePressEvent(QMouseEvent* e) {
int logLeft = width() - _logWidth;
if (qAbs(e->pos().x() - logLeft) < 5 && e->pos().y() > 44) {
_draggingLog = true;
setCursor(Qt::SplitHCursor);
e->accept();
return;
}
QWidget::mousePressEvent(e);
}
void GameWidget::mouseMoveEvent(QMouseEvent* e) {
if (_draggingLog) {
_logWidth = qBound(150, width() - e->pos().x(), 400);
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);
e->accept();
return;
}
int logLeft = width() - _logWidth;
setCursor(qAbs(e->pos().x() - logLeft) < 5 && e->pos().y() > 44 ? Qt::SplitHCursor : Qt::ArrowCursor);
QWidget::mouseMoveEvent(e);
}
void GameWidget::mouseReleaseEvent(QMouseEvent* e) {
if (_draggingLog) {
_draggingLog = false;
setCursor(Qt::ArrowCursor);
e->accept();
return;
}
QWidget::mouseReleaseEvent(e);
}
+5
View File
@@ -38,6 +38,9 @@ protected:
bool eventFilter(QObject* obj, QEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
private:
void initScene();
@@ -99,6 +102,8 @@ private:
ElaLineEdit* _chatInput = nullptr;
ElaPushButton* _chatToggle = nullptr;
bool _chatVisible = false;
int _logWidth = 220;
bool _draggingLog = false;
CardInfoPopup* _cardInfoPopup = nullptr;
ChoiceOverlay* _choiceOverlay = nullptr;
+18 -2
View File
@@ -6,6 +6,7 @@
#include "ElaSpinBox.h"
#include "ElaMessageBar.h"
#include "NetworkManager.h"
#include "SettingsManager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -75,6 +76,21 @@ void LobbyWidget::initUI() {
layout->setContentsMargins(30, 20, 30, 18);
layout->setSpacing(10);
auto* headerRow = new QHBoxLayout();
auto* profileBtn = new ElaPushButton(QStringLiteral("我的"), card);
profileBtn->setFixedSize(60, 26);
connect(profileBtn, &ElaPushButton::clicked, this, [this]() { emit profileRequested(); });
headerRow->addWidget(profileBtn);
headerRow->addStretch();
auto* logoutBtn = new ElaPushButton(QStringLiteral("登出"), card);
logoutBtn->setFixedSize(60, 26);
connect(logoutBtn, &ElaPushButton::clicked, this, [this]() {
NetworkManager::instance().disconnect();
emit logoutRequested();
});
headerRow->addWidget(logoutBtn);
layout->addLayout(headerRow);
auto* titleIcon = new QLabel(card);
titleIcon->setPixmap(QPixmap(":/images/corpse").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
titleIcon->setAlignment(Qt::AlignCenter);
@@ -98,12 +114,12 @@ void LobbyWidget::initUI() {
};
_serverEdit = new ElaLineEdit(card);
_serverEdit->setText("ws://127.0.0.1:8080/ws");
_serverEdit->setText(SettingsManager::instance().getString("network", "server_address", "ws://127.0.0.1:8080/ws"));
_serverEdit->setFixedHeight(30);
makeRow(QStringLiteral("服务器"), _serverEdit);
_nicknameEdit = new ElaLineEdit(card);
_nicknameEdit->setText(QStringLiteral("玩家"));
_nicknameEdit->setText(SettingsManager::instance().getString("general", "nickname", QStringLiteral("玩家")));
_nicknameEdit->setFixedHeight(30);
makeRow(QStringLiteral("昵称"), _nicknameEdit);
+2
View File
@@ -17,6 +17,8 @@ public:
signals:
void joinedRoom();
void createdRoom();
void logoutRequested();
void profileRequested();
protected:
void paintEvent(QPaintEvent* event) override;
+197
View File
@@ -0,0 +1,197 @@
#include "LoginWidget.h"
#include "SettingsManager.h"
#include "NetworkManager.h"
#include "ElaLineEdit.h"
#include "ElaPushButton.h"
#include "ElaToggleSwitch.h"
#include "ElaMessageBar.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QLinearGradient>
LoginWidget::LoginWidget(QWidget* parent) : QWidget(parent) {
initUI();
auto& net = NetworkManager::instance();
connect(&net, &NetworkManager::connected, this, [this]() {
_statusLabel->setText(QStringLiteral("● 已连接"));
_statusLabel->setStyleSheet("color:#5cb85c;font-size:12px;background:transparent;");
_connectBtn->setText(QStringLiteral("断开"));
_connectBtn->setEnabled(true);
_loginBtn->setEnabled(true);
_registerBtn->setEnabled(true);
});
connect(&net, &NetworkManager::disconnected, this, [this]() {
_statusLabel->setText(QStringLiteral("○ 未连接"));
_statusLabel->setStyleSheet("color:#8a8f9d;font-size:12px;background:transparent;");
_connectBtn->setText(QStringLiteral("连接"));
_connectBtn->setEnabled(true);
_loginBtn->setEnabled(false);
_registerBtn->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);
});
auto onResult = [this](const QJsonObject& r) {
bool ok = r["success"].toBool();
if (ok) {
_nickname = r["nickname"].toString();
if (_rememberSwitch->getIsToggled()) {
auto& s = SettingsManager::instance();
s.set("account", "remember", true);
s.set("account", "token", r["token"].toString());
s.set("account", "username", r["username"].toString());
s.set("account", "avatar", r["avatar"].toString());
s.set("account", "bio", r["bio"].toString());
s.save();
}
ElaMessageBar::success(ElaMessageBarType::TopRight, QStringLiteral("成功"), _nickname, 2000, this);
emit loggedIn(_nickname);
} else {
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("失败"), r["message"].toString(), 3000, this);
}
};
connect(&net, &NetworkManager::loginResult, this, onResult);
connect(&net, &NetworkManager::registerResult, this, onResult);
}
void LoginWidget::initUI() {
auto* outer = new QVBoxLayout(this);
outer->setAlignment(Qt::AlignCenter);
auto* card = new QWidget(this);
card->setFixedSize(380, 420);
card->setObjectName("loginCard");
card->setStyleSheet("QWidget#loginCard{background:rgba(37,43,61,230);border-radius:16px;border:1px solid rgba(94,179,230,30);}");
auto* lay = new QVBoxLayout(card);
lay->setContentsMargins(30, 20, 30, 20);
lay->setSpacing(10);
auto* icon = new QLabel(card);
icon->setPixmap(QPixmap(":/images/corpse").scaled(56, 56, Qt::KeepAspectRatio, Qt::SmoothTransformation));
icon->setAlignment(Qt::AlignCenter);
icon->setStyleSheet("background:transparent;");
lay->addWidget(icon);
auto* title = new QLabel(QStringLiteral("冰冷的她醒来之前"), card);
title->setStyleSheet("color:white;font-size:18px;font-weight:bold;background:transparent;");
title->setAlignment(Qt::AlignCenter);
lay->addWidget(title);
lay->addSpacing(8);
auto& s = SettingsManager::instance();
_serverEdit = new ElaLineEdit(card);
_serverEdit->setText(s.getString("network", "server_address", "ws://127.0.0.1:8080/ws"));
_serverEdit->setFixedHeight(30);
_serverEdit->setPlaceholderText(QStringLiteral("服务器地址"));
lay->addWidget(_serverEdit);
auto* connRow = new QHBoxLayout();
_statusLabel = new QLabel(QStringLiteral("○ 未连接"), card);
_statusLabel->setStyleSheet("color:#8a8f9d;font-size:12px;background:transparent;");
_connectBtn = new ElaPushButton(QStringLiteral("连接"), card);
_connectBtn->setFixedSize(70, 28);
connect(_connectBtn, &ElaPushButton::clicked, this, &LoginWidget::onConnect);
connRow->addWidget(_statusLabel);
connRow->addStretch();
connRow->addWidget(_connectBtn);
lay->addLayout(connRow);
_usernameEdit = new ElaLineEdit(card);
_usernameEdit->setPlaceholderText(QStringLiteral("用户名"));
_usernameEdit->setText(s.getString("account", "username"));
_usernameEdit->setFixedHeight(30);
lay->addWidget(_usernameEdit);
_passwordEdit = new ElaLineEdit(card);
_passwordEdit->setPlaceholderText(QStringLiteral("密码"));
_passwordEdit->setEchoMode(QLineEdit::Password);
_passwordEdit->setFixedHeight(30);
lay->addWidget(_passwordEdit);
auto* remRow = new QHBoxLayout();
auto* remLbl = new QLabel(QStringLiteral("记住登录"), card);
remLbl->setStyleSheet("color:#e0e0e0;font-size:12px;background:transparent;");
_rememberSwitch = new ElaToggleSwitch(card);
_rememberSwitch->setIsToggled(s.getBool("account", "remember", false));
remRow->addWidget(remLbl);
remRow->addStretch();
remRow->addWidget(_rememberSwitch);
lay->addLayout(remRow);
auto* btnRow = new QHBoxLayout();
btnRow->setSpacing(10);
_loginBtn = new ElaPushButton(QStringLiteral("登录"), card);
_loginBtn->setFixedHeight(34);
_loginBtn->setEnabled(false);
connect(_loginBtn, &ElaPushButton::clicked, this, &LoginWidget::onLogin);
_registerBtn = new ElaPushButton(QStringLiteral("注册"), card);
_registerBtn->setFixedHeight(34);
_registerBtn->setEnabled(false);
connect(_registerBtn, &ElaPushButton::clicked, this, &LoginWidget::onRegister);
btnRow->addWidget(_loginBtn);
btnRow->addWidget(_registerBtn);
lay->addLayout(btnRow);
lay->addStretch();
outer->addWidget(card);
}
void LoginWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
QLinearGradient grad(0, 0, width(), height());
grad.setColorAt(0, QColor("#1a1f2e"));
grad.setColorAt(1, QColor("#15192a"));
p.fillRect(rect(), grad);
}
QString LoginWidget::nickname() const { return _nickname; }
void LoginWidget::onConnect() {
auto& net = NetworkManager::instance();
if (net.isConnected()) { net.disconnect(); return; }
_connectBtn->setEnabled(false);
_connectBtn->setText(QStringLiteral("连接中..."));
net.connectToServer(_serverEdit->text().trimmed());
}
void LoginWidget::onLogin() {
auto user = _usernameEdit->text().trimmed();
auto pass = _passwordEdit->text();
QJsonObject p;
if (!pass.isEmpty()) {
p["username"] = user;
p["password"] = pass;
} else {
auto token = SettingsManager::instance().getString("account", "token");
if (!token.isEmpty()) {
p["token"] = token;
} else {
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请输入密码"), 2000, this);
return;
}
}
NetworkManager::instance().sendMessage("login", p);
}
void LoginWidget::onRegister() {
auto user = _usernameEdit->text().trimmed();
auto pass = _passwordEdit->text();
if (user.isEmpty() || pass.isEmpty()) {
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请输入用户名和密码"), 2000, this);
return;
}
QJsonObject p;
p["username"] = user;
p["password"] = pass;
p["nickname"] = user;
NetworkManager::instance().sendMessage("register", p);
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef LOGINWIDGET_H
#define LOGINWIDGET_H
#include <QWidget>
class ElaLineEdit;
class ElaPushButton;
class ElaToggleSwitch;
class QLabel;
class LoginWidget : public QWidget {
Q_OBJECT
public:
explicit LoginWidget(QWidget* parent = nullptr);
QString nickname() const;
signals:
void loggedIn(const QString& nickname);
protected:
void paintEvent(QPaintEvent* event) override;
private:
void initUI();
void onConnect();
void onLogin();
void onRegister();
ElaLineEdit* _serverEdit = nullptr;
ElaLineEdit* _usernameEdit = nullptr;
ElaLineEdit* _passwordEdit = nullptr;
ElaToggleSwitch* _rememberSwitch = nullptr;
ElaPushButton* _connectBtn = nullptr;
ElaPushButton* _loginBtn = nullptr;
ElaPushButton* _registerBtn = nullptr;
QLabel* _statusLabel = nullptr;
QString _nickname;
};
#endif
+16 -2
View File
@@ -1,7 +1,10 @@
#include "MainWindow.h"
#include "LoginWidget.h"
#include "LobbyWidget.h"
#include "RoomWidget.h"
#include "GameWidget.h"
#include "ProfileDialog.h"
#include "SettingsManager.h"
#include <QStackedWidget>
#include <QVBoxLayout>
@@ -21,23 +24,34 @@ void MainWindow::initUI() {
layout->setContentsMargins(0, 0, 0, 0);
_stack = new QStackedWidget(this);
_login = new LoginWidget(this);
_lobby = new LobbyWidget(this);
_room = new RoomWidget(this);
_game = new GameWidget(this);
_stack->addWidget(_login);
_stack->addWidget(_lobby);
_stack->addWidget(_room);
_stack->addWidget(_game);
_stack->setCurrentWidget(_lobby);
_stack->setCurrentWidget(_login);
layout->addWidget(_stack);
connect(_login, &LoginWidget::loggedIn, this, &MainWindow::switchToLobby);
connect(_lobby, &LobbyWidget::logoutRequested, this, &MainWindow::switchToLogin);
connect(_lobby, &LobbyWidget::profileRequested, this, [this]() {
auto& s = SettingsManager::instance();
ProfileDialog dlg(s.getString("account", "nickname", s.getString("general", "nickname")),
s.getString("account", "bio"), s.getString("account", "avatar"), this);
dlg.exec();
});
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);
connect(_game, &GameWidget::gameFinished, this, &MainWindow::switchToLobby);
}
void MainWindow::switchToLogin() { _stack->setCurrentWidget(_login); }
void MainWindow::switchToLobby() { _stack->setCurrentWidget(_lobby); }
void MainWindow::switchToRoom() { _stack->setCurrentWidget(_room); }
void MainWindow::switchToGame() { _stack->setCurrentWidget(_game); }
+3
View File
@@ -4,6 +4,7 @@
#include <QWidget>
class QStackedWidget;
class LoginWidget;
class LobbyWidget;
class RoomWidget;
class GameWidget;
@@ -13,6 +14,7 @@ class MainWindow : public QWidget {
public:
explicit MainWindow(QWidget* parent = nullptr);
void switchToLogin();
void switchToLobby();
void switchToRoom();
void switchToGame();
@@ -21,6 +23,7 @@ private:
void initUI();
QStackedWidget* _stack = nullptr;
LoginWidget* _login = nullptr;
LobbyWidget* _lobby = nullptr;
RoomWidget* _room = nullptr;
GameWidget* _game = nullptr;
+111
View File
@@ -0,0 +1,111 @@
#include "ProfileDialog.h"
#include "NetworkManager.h"
#include "ElaLineEdit.h"
#include "ElaPushButton.h"
#include "ElaMessageBar.h"
#include "CardData.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QGridLayout>
#include <QEvent>
ProfileDialog::ProfileDialog(const QString& nickname, const QString& bio, const QString& avatar, QWidget* parent)
: QDialog(parent), _avatar(avatar)
{
setWindowTitle(QStringLiteral("我的资料"));
setFixedSize(480, 440);
setStyleSheet("QDialog{background:#1a1f2e;}QLabel{color:#e0e0e0;background:transparent;}");
auto* lay = new QVBoxLayout(this);
lay->setSpacing(10);
auto* title = new QLabel(QStringLiteral("个人资料"), this);
title->setStyleSheet("color:#5eb3e6;font-size:18px;font-weight:bold;");
title->setAlignment(Qt::AlignCenter);
lay->addWidget(title);
_avatarLabel = new QLabel(this);
_avatarLabel->setFixedSize(80, 80);
_avatarLabel->setAlignment(Qt::AlignCenter);
_avatarLabel->setStyleSheet("border:2px solid #5eb3e6;border-radius:8px;");
if (!avatar.isEmpty()) {
QPixmap px = CardDatabase::instance().getCardFrontImage(avatar);
if (!px.isNull()) _avatarLabel->setPixmap(px.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
lay->addWidget(_avatarLabel, 0, Qt::AlignCenter);
auto* avLabel = new QLabel(QStringLiteral("选择头像 (点击卡牌)"), this);
avLabel->setStyleSheet("color:#8a8f9d;font-size:11px;");
avLabel->setAlignment(Qt::AlignCenter);
lay->addWidget(avLabel);
auto* grid = new QGridLayout();
grid->setSpacing(6);
QStringList ids = {"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 (int i = 0; i < ids.size(); ++i) {
auto* btn = new QLabel(this);
btn->setFixedSize(42, 42);
btn->setCursor(Qt::PointingHandCursor);
QPixmap px = CardDatabase::instance().getCardFrontImage(ids[i]);
if (!px.isNull()) btn->setPixmap(px.scaled(38, 38, Qt::KeepAspectRatio, Qt::SmoothTransformation));
btn->setStyleSheet(ids[i] == _avatar ? "border:2px solid #5eb3e6;border-radius:4px;" : "border:1px solid #3a3f4e;border-radius:4px;");
btn->setProperty("avid", ids[i]);
btn->installEventFilter(this);
grid->addWidget(btn, i / 7, i % 7);
}
lay->addLayout(grid);
auto* row1 = new QHBoxLayout();
row1->addWidget(new QLabel(QStringLiteral("昵称"), this));
_nickEdit = new ElaLineEdit(this); _nickEdit->setText(nickname); _nickEdit->setFixedHeight(28);
row1->addWidget(_nickEdit, 1);
lay->addLayout(row1);
auto* row2 = new QHBoxLayout();
row2->addWidget(new QLabel(QStringLiteral("简介"), this));
_bioEdit = new ElaLineEdit(this); _bioEdit->setText(bio); _bioEdit->setFixedHeight(28);
row2->addWidget(_bioEdit, 1);
lay->addLayout(row2);
auto* btnRow = new QHBoxLayout();
btnRow->addStretch();
auto* saveBtn = new ElaPushButton(QStringLiteral("保存"), this);
saveBtn->setFixedSize(80, 32);
connect(saveBtn, &ElaPushButton::clicked, this, &ProfileDialog::save);
auto* closeBtn = new ElaPushButton(QStringLiteral("关闭"), this);
closeBtn->setFixedSize(80, 32);
connect(closeBtn, &ElaPushButton::clicked, this, &QDialog::accept);
btnRow->addWidget(saveBtn);
btnRow->addWidget(closeBtn);
lay->addLayout(btnRow);
}
bool ProfileDialog::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::MouseButtonPress) {
auto* lbl = qobject_cast<QLabel*>(obj);
if (lbl && lbl->property("avid").isValid()) {
_avatar = lbl->property("avid").toString();
QPixmap px = CardDatabase::instance().getCardFrontImage(_avatar);
if (!px.isNull()) _avatarLabel->setPixmap(px.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
for (auto* child : findChildren<QLabel*>()) {
if (child->property("avid").isValid())
child->setStyleSheet(child->property("avid").toString() == _avatar
? "border:2px solid #5eb3e6;border-radius:4px;" : "border:1px solid #3a3f4e;border-radius:4px;");
}
return true;
}
}
return QDialog::eventFilter(obj, event);
}
void ProfileDialog::save() {
QJsonObject p;
p["nickname"] = _nickEdit->text();
p["bio"] = _bioEdit->text();
p["avatar"] = _avatar;
NetworkManager::instance().sendMessage("update_profile", p);
ElaMessageBar::success(ElaMessageBarType::TopRight, QStringLiteral("保存"), QStringLiteral("资料已更新"), 2000, this);
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef PROFILEDIALOG_H
#define PROFILEDIALOG_H
#include <QDialog>
class ElaLineEdit;
class ElaPushButton;
class QLabel;
class ProfileDialog : public QDialog {
Q_OBJECT
public:
explicit ProfileDialog(const QString& nickname, const QString& bio, const QString& avatar, QWidget* parent = nullptr);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private:
void save();
ElaLineEdit* _nickEdit = nullptr;
ElaLineEdit* _bioEdit = nullptr;
QString _avatar;
QLabel* _avatarLabel = nullptr;
};
#endif
+5
View File
@@ -73,6 +73,11 @@ void RoomWidget::initUI() {
auto* chaosLbl = new QLabel(QStringLiteral("混沌模式"), card);
chaosLbl->setStyleSheet("color: #999; font-size: 12px; background:transparent;");
_chaosSwitch = new ElaToggleSwitch(card);
connect(_chaosSwitch, &ElaToggleSwitch::toggled, this, [](bool checked) {
QJsonObject payload;
payload["enabled"] = checked;
NetworkManager::instance().sendMessage("set_chaos", payload);
});
chaosRow->addWidget(chaosLbl);
chaosRow->addStretch();
chaosRow->addWidget(_chaosSwitch);
+1 -1
View File
@@ -85,7 +85,7 @@ void SceneCardItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*
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->setFont(QFont("monospace", 11, QFont::Bold));
p->setPen(def->mp >= 0 ? QColor("#d4c5a3") : QColor("#d94f5c"));
p->drawText(badge, Qt::AlignCenter, QString::number(def->mp));
}
+344
View File
@@ -0,0 +1,344 @@
#include "SettingsDialog.h"
#include "SettingsManager.h"
#include "VoiceManager.h"
#include "ElaLineEdit.h"
#include "ElaPushButton.h"
#include "ElaSlider.h"
#include "ElaToggleSwitch.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QStackedWidget>
#include <QComboBox>
#include <QRadioButton>
#include <QGroupBox>
#include <QMessageBox>
#include <QCloseEvent>
#include <QMediaDevices>
#include <QAudioDevice>
static const QString SS_PAGE = "background:transparent;";
static const QString SS_TITLE = "color:#5eb3e6;font-size:16px;font-weight:bold;background:transparent;";
static const QString SS_LABEL = "color:#e0e0e0;font-size:13px;background:transparent;";
static const QString SS_DIM = "color:#8a8f9d;font-size:11px;background:transparent;";
SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent)
{
setWindowTitle(QStringLiteral("设置"));
setFixedSize(620, 460);
setStyleSheet("QDialog{background:#1a1f2e;} QGroupBox{color:#8a8f9d;border:1px solid #2a3040;border-radius:8px;margin-top:10px;padding-top:16px;} QGroupBox::title{subcontrol-origin:margin;left:12px;padding:0 4px;}");
auto* root = new QHBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
_categoryList = new QListWidget(this);
_categoryList->setFixedWidth(140);
_categoryList->setStyleSheet("QListWidget{background:#151a28;border:none;border-right:1px solid #2a3040;color:#e0e0e0;font-size:14px;} QListWidget::item{padding:12px 16px;} QListWidget::item:selected{background:#252b3d;color:#5eb3e6;border-left:3px solid #5eb3e6;}");
_categoryList->addItem(QStringLiteral("通用"));
_categoryList->addItem(QStringLiteral("网络"));
_categoryList->addItem(QStringLiteral("语音"));
_categoryList->addItem(QStringLiteral("音频"));
_categoryList->addItem(QStringLiteral("显示"));
root->addWidget(_categoryList);
auto* rightPanel = new QWidget(this);
auto* rLay = new QVBoxLayout(rightPanel);
rLay->setContentsMargins(16, 12, 16, 12);
rLay->setSpacing(8);
_pages = new QStackedWidget(rightPanel);
_pages->addWidget(buildGeneralPage());
_pages->addWidget(buildNetworkPage());
_pages->addWidget(buildVoicePage());
_pages->addWidget(buildAudioPage());
_pages->addWidget(buildDisplayPage());
rLay->addWidget(_pages, 1);
auto* btnRow = new QHBoxLayout();
auto* resetBtn = new ElaPushButton(QStringLiteral("恢复默认"), rightPanel);
resetBtn->setFixedHeight(32);
connect(resetBtn, &ElaPushButton::clicked, this, [this]() {
SettingsManager::instance().resetToDefaults();
loadFromConfig();
_dirty = true;
});
auto* saveBtn = new ElaPushButton(QStringLiteral("保存"), rightPanel);
saveBtn->setFixedHeight(32);
connect(saveBtn, &ElaPushButton::clicked, this, [this]() {
saveToConfig();
SettingsManager::instance().save();
_dirty = false;
accept();
});
auto* cancelBtn = new ElaPushButton(QStringLiteral("取消"), rightPanel);
cancelBtn->setFixedHeight(32);
connect(cancelBtn, &ElaPushButton::clicked, this, &QDialog::reject);
btnRow->addWidget(resetBtn);
btnRow->addStretch();
btnRow->addWidget(saveBtn);
btnRow->addWidget(cancelBtn);
rLay->addLayout(btnRow);
root->addWidget(rightPanel, 1);
connect(_categoryList, &QListWidget::currentRowChanged, _pages, &QStackedWidget::setCurrentIndex);
_categoryList->setCurrentRow(0);
loadFromConfig();
}
QWidget* SettingsDialog::buildGeneralPage() {
auto* page = new QWidget();
page->setStyleSheet(SS_PAGE);
auto* lay = new QVBoxLayout(page);
auto* title = new QLabel(QStringLiteral("通用设置"), page);
title->setStyleSheet(SS_TITLE);
lay->addWidget(title);
auto* grp = new QGroupBox(QStringLiteral("玩家信息"), page);
auto* gLay = new QVBoxLayout(grp);
auto* row = new QHBoxLayout();
auto* lbl = new QLabel(QStringLiteral("默认昵称"), grp);
lbl->setStyleSheet(SS_LABEL);
_nicknameEdit = new ElaLineEdit(grp);
_nicknameEdit->setFixedHeight(30);
connect(_nicknameEdit, &ElaLineEdit::textChanged, this, [this]() { _dirty = true; });
row->addWidget(lbl);
row->addWidget(_nicknameEdit, 1);
gLay->addLayout(row);
lay->addWidget(grp);
lay->addStretch();
return page;
}
QWidget* SettingsDialog::buildNetworkPage() {
auto* page = new QWidget();
page->setStyleSheet(SS_PAGE);
auto* lay = new QVBoxLayout(page);
auto* title = new QLabel(QStringLiteral("网络设置"), page);
title->setStyleSheet(SS_TITLE);
lay->addWidget(title);
auto* grp = new QGroupBox(QStringLiteral("连接"), page);
auto* gLay = new QVBoxLayout(grp);
auto* row = new QHBoxLayout();
auto* lbl = new QLabel(QStringLiteral("默认服务器"), grp);
lbl->setStyleSheet(SS_LABEL);
_serverEdit = new ElaLineEdit(grp);
_serverEdit->setFixedHeight(30);
connect(_serverEdit, &ElaLineEdit::textChanged, this, [this]() { _dirty = true; });
row->addWidget(lbl);
row->addWidget(_serverEdit, 1);
gLay->addLayout(row);
auto* hint = new QLabel(QStringLiteral("格式: ws://地址:端口/ws"), grp);
hint->setStyleSheet(SS_DIM);
gLay->addWidget(hint);
lay->addWidget(grp);
lay->addStretch();
return page;
}
QWidget* SettingsDialog::buildVoicePage() {
auto* page = new QWidget();
page->setStyleSheet(SS_PAGE);
auto* lay = new QVBoxLayout(page);
auto* title = new QLabel(QStringLiteral("语音设置"), page);
title->setStyleSheet(SS_TITLE);
lay->addWidget(title);
auto* devGrp = new QGroupBox(QStringLiteral("设备"), page);
auto* dLay = new QVBoxLayout(devGrp);
auto* inRow = new QHBoxLayout();
auto* inLbl = new QLabel(QStringLiteral("输入 (麦克风)"), devGrp);
inLbl->setStyleSheet(SS_LABEL);
_inputDevCombo = new QComboBox(devGrp);
for (const auto& d : QMediaDevices::audioInputs()) _inputDevCombo->addItem(d.description());
if (_inputDevCombo->count() == 0) _inputDevCombo->addItem(QStringLiteral("(无)"));
connect(_inputDevCombo, &QComboBox::currentIndexChanged, this, [this]() { _dirty = true; });
inRow->addWidget(inLbl);
inRow->addWidget(_inputDevCombo, 1);
dLay->addLayout(inRow);
auto* outRow = new QHBoxLayout();
auto* outLbl = new QLabel(QStringLiteral("输出 (扬声器)"), devGrp);
outLbl->setStyleSheet(SS_LABEL);
_outputDevCombo = new QComboBox(devGrp);
for (const auto& d : QMediaDevices::audioOutputs()) _outputDevCombo->addItem(d.description());
if (_outputDevCombo->count() == 0) _outputDevCombo->addItem(QStringLiteral("(无)"));
connect(_outputDevCombo, &QComboBox::currentIndexChanged, this, [this]() { _dirty = true; });
outRow->addWidget(outLbl);
outRow->addWidget(_outputDevCombo, 1);
dLay->addLayout(outRow);
lay->addWidget(devGrp);
auto* modeGrp = new QGroupBox(QStringLiteral("说话模式"), page);
auto* mLay = new QVBoxLayout(modeGrp);
_pttRadio = new QRadioButton(QStringLiteral("按住说话 — 按住 V 键时可说话,松开停止"), modeGrp);
_toggleRadio = new QRadioButton(QStringLiteral("按键切换 — 按 V 键开始/停止说话"), modeGrp);
_pttRadio->setStyleSheet(SS_LABEL);
_toggleRadio->setStyleSheet(SS_LABEL);
connect(_pttRadio, &QRadioButton::toggled, this, [this]() { _dirty = true; });
mLay->addWidget(_pttRadio);
mLay->addWidget(_toggleRadio);
auto* hint = new QLabel(QStringLiteral("在聊天输入框输入时 V 键不触发语音"), modeGrp);
hint->setStyleSheet(SS_DIM);
mLay->addWidget(hint);
lay->addWidget(modeGrp);
lay->addStretch();
return page;
}
QWidget* SettingsDialog::buildAudioPage() {
auto* page = new QWidget();
page->setStyleSheet(SS_PAGE);
auto* lay = new QVBoxLayout(page);
auto* title = new QLabel(QStringLiteral("音频设置"), page);
title->setStyleSheet(SS_TITLE);
lay->addWidget(title);
auto* grp = new QGroupBox(QStringLiteral("音量"), page);
auto* gLay = new QVBoxLayout(grp);
auto* mRow = new QHBoxLayout();
auto* mLbl = new QLabel(QStringLiteral("主音量"), grp);
mLbl->setStyleSheet(SS_LABEL);
_masterVolSlider = new ElaSlider(Qt::Horizontal, grp);
_masterVolSlider->setRange(0, 100);
_masterVolLabel = new QLabel("80", grp);
_masterVolLabel->setStyleSheet(SS_LABEL);
_masterVolLabel->setFixedWidth(30);
connect(_masterVolSlider, &ElaSlider::valueChanged, this, [this](int v) {
_masterVolLabel->setText(QString::number(v));
_dirty = true;
});
mRow->addWidget(mLbl);
mRow->addWidget(_masterVolSlider, 1);
mRow->addWidget(_masterVolLabel);
gLay->addLayout(mRow);
auto* vRow = new QHBoxLayout();
auto* vLbl = new QLabel(QStringLiteral("语音音量"), grp);
vLbl->setStyleSheet(SS_LABEL);
_voiceVolSlider = new ElaSlider(Qt::Horizontal, grp);
_voiceVolSlider->setRange(0, 100);
_voiceVolLabel = new QLabel("100", grp);
_voiceVolLabel->setStyleSheet(SS_LABEL);
_voiceVolLabel->setFixedWidth(30);
connect(_voiceVolSlider, &ElaSlider::valueChanged, this, [this](int v) {
_voiceVolLabel->setText(QString::number(v));
_dirty = true;
});
vRow->addWidget(vLbl);
vRow->addWidget(_voiceVolSlider, 1);
vRow->addWidget(_voiceVolLabel);
gLay->addLayout(vRow);
lay->addWidget(grp);
lay->addStretch();
return page;
}
QWidget* SettingsDialog::buildDisplayPage() {
auto* page = new QWidget();
page->setStyleSheet(SS_PAGE);
auto* lay = new QVBoxLayout(page);
auto* title = new QLabel(QStringLiteral("显示设置"), page);
title->setStyleSheet(SS_TITLE);
lay->addWidget(title);
auto* grp = new QGroupBox(QStringLiteral("动画"), page);
auto* gLay = new QVBoxLayout(grp);
auto* row = new QHBoxLayout();
auto* lbl = new QLabel(QStringLiteral("启用动画效果"), grp);
lbl->setStyleSheet(SS_LABEL);
_animSwitch = new ElaToggleSwitch(grp);
connect(_animSwitch, &ElaToggleSwitch::toggled, this, [this]() { _dirty = true; });
row->addWidget(lbl);
row->addStretch();
row->addWidget(_animSwitch);
gLay->addLayout(row);
lay->addWidget(grp);
lay->addStretch();
return page;
}
void SettingsDialog::loadFromConfig() {
auto& s = SettingsManager::instance();
_nicknameEdit->setText(s.getString("general", "nickname", "Player"));
_serverEdit->setText(s.getString("network", "server_address", "ws://127.0.0.1:8080/ws"));
auto mode = s.getString("voice", "mode", "ptt");
_pttRadio->setChecked(mode == "ptt");
_toggleRadio->setChecked(mode != "ptt");
auto savedIn = s.getString("voice", "input_device");
if (!savedIn.isEmpty()) {
int idx = _inputDevCombo->findText(savedIn);
if (idx >= 0) _inputDevCombo->setCurrentIndex(idx);
}
auto savedOut = s.getString("voice", "output_device");
if (!savedOut.isEmpty()) {
int idx = _outputDevCombo->findText(savedOut);
if (idx >= 0) _outputDevCombo->setCurrentIndex(idx);
}
_masterVolSlider->setValue(s.getInt("audio", "master_volume", 80));
_masterVolLabel->setText(QString::number(_masterVolSlider->value()));
_voiceVolSlider->setValue(s.getInt("audio", "voice_volume", 100));
_voiceVolLabel->setText(QString::number(_voiceVolSlider->value()));
_animSwitch->setIsToggled(s.getBool("display", "animations_enabled", true));
_dirty = false;
}
void SettingsDialog::saveToConfig() {
auto& s = SettingsManager::instance();
s.set("general", "nickname", _nicknameEdit->text());
s.set("network", "server_address", _serverEdit->text());
s.set("voice", "mode", _pttRadio->isChecked() ? "ptt" : "toggle");
s.set("voice", "input_device", _inputDevCombo->currentText());
s.set("voice", "output_device", _outputDevCombo->currentText());
s.set("audio", "master_volume", _masterVolSlider->value());
s.set("audio", "voice_volume", _voiceVolSlider->value());
s.set("display", "animations_enabled", _animSwitch->getIsToggled());
auto& v = VoiceManager::instance();
v.setMode(_pttRadio->isChecked() ? VoiceManager::PushToTalk : VoiceManager::Toggle);
for (const auto& d : QMediaDevices::audioInputs()) {
if (d.description() == _inputDevCombo->currentText()) {
v.setInputDevice(d); break;
}
}
for (const auto& d : QMediaDevices::audioOutputs()) {
if (d.description() == _outputDevCombo->currentText()) {
v.setOutputDevice(d); break;
}
}
if (!v.isActive()) v.start();
emit SettingsManager::instance().settingsChanged();
}
bool SettingsDialog::hasChanges() const { return _dirty; }
void SettingsDialog::closeEvent(QCloseEvent* e) {
if (_dirty) {
auto r = QMessageBox::question(const_cast<SettingsDialog*>(this),
QStringLiteral("未保存"), QStringLiteral("设置已修改但尚未保存,是否保存?"),
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
if (r == QMessageBox::Save) {
saveToConfig();
SettingsManager::instance().save();
e->accept();
} else if (r == QMessageBox::Discard) {
e->accept();
} else {
e->ignore();
}
} else {
e->accept();
}
}
+53
View File
@@ -0,0 +1,53 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
#include <QJsonObject>
class QListWidget;
class QStackedWidget;
class ElaLineEdit;
class ElaSlider;
class ElaToggleSwitch;
class QComboBox;
class QRadioButton;
class QLabel;
class SettingsDialog : public QDialog {
Q_OBJECT
public:
explicit SettingsDialog(QWidget* parent = nullptr);
protected:
void closeEvent(QCloseEvent* event) override;
private:
QWidget* buildGeneralPage();
QWidget* buildNetworkPage();
QWidget* buildVoicePage();
QWidget* buildAudioPage();
QWidget* buildDisplayPage();
void loadFromConfig();
void saveToConfig();
bool hasChanges() const;
QListWidget* _categoryList = nullptr;
QStackedWidget* _pages = nullptr;
ElaLineEdit* _nicknameEdit = nullptr;
ElaLineEdit* _serverEdit = nullptr;
QComboBox* _inputDevCombo = nullptr;
QComboBox* _outputDevCombo = nullptr;
QRadioButton* _pttRadio = nullptr;
QRadioButton* _toggleRadio = nullptr;
ElaSlider* _masterVolSlider = nullptr;
ElaSlider* _voiceVolSlider = nullptr;
ElaToggleSwitch* _animSwitch = nullptr;
QLabel* _masterVolLabel = nullptr;
QLabel* _voiceVolLabel = nullptr;
QJsonObject _original;
bool _dirty = false;
};
#endif
+6
View File
@@ -4,6 +4,7 @@
#include "ElaPushButton.h"
#include <QVBoxLayout>
#include "SoundManager.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
@@ -140,6 +141,7 @@ void SettlementOverlay::revealNextCard() {
auto c = cards[_revealIdx].toObject();
auto* cw = makeFlipCard(c);
_cardRow->addWidget(cw);
SoundManager::instance().play("card_flip");
_runningTotal += c["mp"].toInt();
int target = _data["harmony"].toObject()["target"].toInt();
@@ -213,6 +215,7 @@ void SettlementOverlay::showPhaseResult() {
_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()));
SoundManager::instance().play(ok ? "settle_harmony_ok" : "settle_harmony_fail");
} else if (_phase == 1) {
auto imprisoned = _data["challenge"].toObject()["imprisoned"].toArray();
if (imprisoned.isEmpty()) {
@@ -228,18 +231,21 @@ void SettlementOverlay::showPhaseResult() {
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()));
SoundManager::instance().play("settle_prison");
}
} 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()));
SoundManager::instance().play("settle_alldead");
} 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()));
SoundManager::instance().play("settle_victory");
}
_nextBtn->setText(QStringLiteral("查看详情"));
}
+65
View File
@@ -0,0 +1,65 @@
#include "SpectrumWidget.h"
#include <QPainter>
#include <QLinearGradient>
SpectrumWidget::SpectrumWidget(int barCount, QWidget* parent)
: QWidget(parent), _barCount(barCount), _current(barCount, 0), _peak(barCount, 0)
{
setFixedSize(100, 26);
_decayTimer = new QTimer(this);
_decayTimer->setInterval(33);
connect(_decayTimer, &QTimer::timeout, this, [this]() {
bool anyActive = false;
for (int i = 0; i < _barCount; ++i) {
_peak[i] *= 0.88f;
if (_peak[i] < 0.01f) _peak[i] = 0;
else anyActive = true;
}
if (!anyActive) _decayTimer->stop();
update();
});
}
void SpectrumWidget::updateLevels(const QVector<float>& levels) {
for (int i = 0; i < _barCount && i < levels.size(); ++i) {
_current[i] = levels[i];
if (levels[i] > _peak[i]) _peak[i] = levels[i];
}
if (!_decayTimer->isActive()) _decayTimer->start();
update();
}
void SpectrumWidget::reset() {
_current.fill(0);
_peak.fill(0);
_decayTimer->stop();
update();
}
void SpectrumWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
int w = width(), h = height();
qreal gap = 1.5;
qreal barW = (w - (_barCount - 1) * gap) / _barCount;
for (int i = 0; i < _barCount; ++i) {
qreal x = i * (barW + gap);
qreal level = qMin(1.0f, _peak[i]);
qreal barH = qMax(2.0, level * h);
QLinearGradient grad(0, h, 0, h - barH);
if (level > 0.01) {
grad.setColorAt(0, QColor(94, 179, 230, 200));
grad.setColorAt(1, QColor(92, 184, 92, 240));
} else {
grad.setColorAt(0, QColor(42, 48, 64, 100));
grad.setColorAt(1, QColor(42, 48, 64, 60));
}
p.setPen(Qt::NoPen);
p.setBrush(grad);
p.drawRoundedRect(QRectF(x, h - barH, barW, barH), 1.5, 1.5);
}
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef SPECTRUMWIDGET_H
#define SPECTRUMWIDGET_H
#include <QWidget>
#include <QVector>
#include <QTimer>
class SpectrumWidget : public QWidget {
Q_OBJECT
public:
explicit SpectrumWidget(int barCount = 12, QWidget* parent = nullptr);
void updateLevels(const QVector<float>& levels);
void reset();
protected:
void paintEvent(QPaintEvent* event) override;
private:
int _barCount;
QVector<float> _current;
QVector<float> _peak;
QTimer* _decayTimer = nullptr;
};
#endif
-97
View File
@@ -1,97 +0,0 @@
#include "VoiceSettingsDialog.h"
#include "VoiceManager.h"
#include "ElaPushButton.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QRadioButton>
#include <QGroupBox>
#include <QMediaDevices>
#include <QAudioDevice>
VoiceSettingsDialog::VoiceSettingsDialog(QWidget* parent)
: QDialog(parent)
{
setWindowTitle(QStringLiteral("语音设置"));
setFixedSize(400, 340);
setStyleSheet("QDialog{background:#1a1f2e;} QLabel{color:#e0e0e0;} QGroupBox{color:#8a8f9d;border:1px solid #3a3f4e;border-radius:6px;margin-top:8px;padding-top:14px;} QGroupBox::title{subcontrol-origin:margin;left:10px;}");
auto* lay = new QVBoxLayout(this);
lay->setSpacing(12);
auto* inputGroup = new QGroupBox(QStringLiteral("输入设备 (麦克风)"), this);
auto* igLay = new QVBoxLayout(inputGroup);
_inputCombo = new QComboBox(inputGroup);
igLay->addWidget(_inputCombo);
lay->addWidget(inputGroup);
auto* outputGroup = new QGroupBox(QStringLiteral("输出设备 (扬声器)"), this);
auto* ogLay = new QVBoxLayout(outputGroup);
_outputCombo = new QComboBox(outputGroup);
ogLay->addWidget(_outputCombo);
lay->addWidget(outputGroup);
auto* modeGroup = new QGroupBox(QStringLiteral("说话模式"), this);
auto* mgLay = new QVBoxLayout(modeGroup);
_pttRadio = new QRadioButton(QStringLiteral("按住说话 (按住 V 键说话,松开停止)"), modeGroup);
_toggleRadio = new QRadioButton(QStringLiteral("按键切换 (按 V 键开始/停止说话)"), modeGroup);
_pttRadio->setStyleSheet("color:#e0e0e0;");
_toggleRadio->setStyleSheet("color:#e0e0e0;");
mgLay->addWidget(_pttRadio);
mgLay->addWidget(_toggleRadio);
lay->addWidget(modeGroup);
_statusLabel = new QLabel(this);
_statusLabel->setStyleSheet("color:#5eb3e6;font-size:12px;");
_statusLabel->setAlignment(Qt::AlignCenter);
lay->addWidget(_statusLabel);
auto* btnRow = new QHBoxLayout();
btnRow->addStretch();
auto* applyBtn = new ElaPushButton(QStringLiteral("应用"), this);
applyBtn->setFixedSize(80, 34);
connect(applyBtn, &ElaPushButton::clicked, this, &VoiceSettingsDialog::apply);
auto* closeBtn = new ElaPushButton(QStringLiteral("关闭"), this);
closeBtn->setFixedSize(80, 34);
connect(closeBtn, &ElaPushButton::clicked, this, &QDialog::close);
btnRow->addWidget(applyBtn);
btnRow->addWidget(closeBtn);
lay->addLayout(btnRow);
populate();
}
void VoiceSettingsDialog::populate() {
_inputCombo->clear();
for (const auto& dev : QMediaDevices::audioInputs())
_inputCombo->addItem(dev.description(), QVariant::fromValue(dev));
if (_inputCombo->count() == 0)
_inputCombo->addItem(QStringLiteral("(无可用设备)"));
_outputCombo->clear();
for (const auto& dev : QMediaDevices::audioOutputs())
_outputCombo->addItem(dev.description(), QVariant::fromValue(dev));
if (_outputCombo->count() == 0)
_outputCombo->addItem(QStringLiteral("(无可用设备)"));
auto& v = VoiceManager::instance();
_pttRadio->setChecked(v.mode() == VoiceManager::PushToTalk);
_toggleRadio->setChecked(v.mode() == VoiceManager::Toggle);
_statusLabel->setText(v.isActive() ? QStringLiteral("语音已启动") : QStringLiteral("语音未启动"));
}
void VoiceSettingsDialog::apply() {
auto& v = VoiceManager::instance();
v.setMode(_pttRadio->isChecked() ? VoiceManager::PushToTalk : VoiceManager::Toggle);
auto inDev = _inputCombo->currentData().value<QAudioDevice>();
auto outDev = _outputCombo->currentData().value<QAudioDevice>();
if (!inDev.isNull()) v.setInputDevice(inDev);
if (!outDev.isNull()) v.setOutputDevice(outDev);
if (!v.isActive()) v.start();
_statusLabel->setText(QStringLiteral("已应用,语音已启动"));
}
-26
View File
@@ -1,26 +0,0 @@
#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
+14
View File
@@ -121,6 +121,20 @@ void VoiceManager::onCaptureReady() {
if (!_muted && speaking)
emit audioFrame(data);
const int BANDS = 12;
QVector<float> levels(BANDS, 0);
int samplesPerBand = qMax(1, samples / BANDS);
for (int b = 0; b < BANDS; ++b) {
double bandEnergy = 0;
int start = b * samplesPerBand;
int end = qMin(start + samplesPerBand, samples);
for (int i = start; i < end; ++i)
bandEnergy += double(pcm[i]) * pcm[i];
int count = end - start;
levels[b] = count > 0 ? float(std::sqrt(bandEnergy / count) / 16000.0) : 0;
}
emit spectrumData(levels);
}
void VoiceManager::processRemoteAudio(const QByteArray& frame) {
+1
View File
@@ -42,6 +42,7 @@ signals:
void localSpeaking(bool speaking);
void remoteSpeaking(const QString& playerId, bool speaking);
void mutedChanged(bool muted);
void spectrumData(const QVector<float>& levels);
private:
explicit VoiceManager(QObject* parent = nullptr);