增加和修改了非常多的功能
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ public:
|
||||
signals:
|
||||
void joinedRoom();
|
||||
void createdRoom();
|
||||
void logoutRequested();
|
||||
void profileRequested();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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("查看详情"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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("已应用,语音已启动"));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user