372 lines
12 KiB
C++
372 lines
12 KiB
C++
#include "InspectWindow.h"
|
|
|
|
#include <cmath>
|
|
|
|
#include <QCloseEvent>
|
|
#include <QHBoxLayout>
|
|
#include <QKeyEvent>
|
|
#include <QSignalMapper>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "ArenaView.h"
|
|
#include "EntityAdmin.h"
|
|
#include "EventManager.h"
|
|
#include "HealthComponent.h"
|
|
#include "InspectWindowClosedEvent.h"
|
|
#include "ModuleOwnerComponent.h"
|
|
#include "ShipIdentityComponent.h"
|
|
#include "ShipStatsCalculator.h"
|
|
#include "ShipStatsPanel.h"
|
|
#include "StationBodyComponent.h"
|
|
#include "WeaponComponent.h"
|
|
|
|
const double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
|
const int InspectWindow::kSpeedCount = 5;
|
|
|
|
InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
|
const VisualsConfig* visuals,
|
|
const std::string& arenaName, QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_sim(sim)
|
|
, m_config(config)
|
|
{
|
|
setWindowTitle(tr("Inspect \u2014 %1").arg(QString::fromStdString(arenaName)));
|
|
resize(900, 700);
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
mainLayout->setSpacing(0);
|
|
|
|
// Header: arena name + speed buttons
|
|
{
|
|
QWidget* header = new QWidget(this);
|
|
QHBoxLayout* headerLayout = new QHBoxLayout(header);
|
|
headerLayout->setContentsMargins(8, 4, 8, 4);
|
|
headerLayout->setSpacing(8);
|
|
|
|
QLabel* nameLabel = new QLabel(QString::fromStdString(arenaName), header);
|
|
QFont nameFont = nameLabel->font();
|
|
nameFont.setBold(true);
|
|
nameFont.setPointSize(nameFont.pointSize() + 2);
|
|
nameLabel->setFont(nameFont);
|
|
headerLayout->addWidget(nameLabel);
|
|
|
|
headerLayout->addStretch();
|
|
|
|
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
|
|
QSignalMapper* mapper = new QSignalMapper(this);
|
|
for (int i = 0; i < kSpeedCount; ++i)
|
|
{
|
|
QPushButton* btn = new QPushButton(labels[i], header);
|
|
btn->setCheckable(true);
|
|
btn->setChecked(i == 2);
|
|
headerLayout->addWidget(btn);
|
|
m_speedButtons.push_back(btn);
|
|
mapper->setMapping(btn, i);
|
|
connect(btn, &QPushButton::clicked,
|
|
mapper, qOverload<>(&QSignalMapper::map));
|
|
}
|
|
connect(mapper, qOverload<int>(&QSignalMapper::mapped),
|
|
this, &InspectWindow::onSpeedButton);
|
|
|
|
header->setFixedHeight(header->sizeHint().height());
|
|
mainLayout->addWidget(header);
|
|
}
|
|
|
|
// Arena view (center, stretch)
|
|
m_arenaView = new ArenaView(sim, visuals, this);
|
|
mainLayout->addWidget(m_arenaView, 1);
|
|
|
|
// Info panel (bottom)
|
|
{
|
|
QWidget* infoPanel = new QWidget(this);
|
|
QHBoxLayout* infoLayout = new QHBoxLayout(infoPanel);
|
|
infoLayout->setContentsMargins(8, 4, 8, 4);
|
|
infoLayout->setSpacing(16);
|
|
|
|
QVBoxLayout* team1Layout = new QVBoxLayout();
|
|
m_team1Header = new QLabel(infoPanel);
|
|
QFont headerFont = m_team1Header->font();
|
|
headerFont.setBold(true);
|
|
m_team1Header->setFont(headerFont);
|
|
team1Layout->addWidget(m_team1Header);
|
|
m_team1Threat = new QLabel(infoPanel);
|
|
team1Layout->addWidget(m_team1Threat);
|
|
m_team1Content = new QLabel(infoPanel);
|
|
team1Layout->addWidget(m_team1Content);
|
|
team1Layout->addStretch();
|
|
infoLayout->addLayout(team1Layout);
|
|
|
|
QVBoxLayout* team2Layout = new QVBoxLayout();
|
|
m_team2Header = new QLabel(infoPanel);
|
|
m_team2Header->setFont(headerFont);
|
|
team2Layout->addWidget(m_team2Header);
|
|
m_team2Threat = new QLabel(infoPanel);
|
|
team2Layout->addWidget(m_team2Threat);
|
|
m_team2Content = new QLabel(infoPanel);
|
|
team2Layout->addWidget(m_team2Content);
|
|
team2Layout->addStretch();
|
|
infoLayout->addLayout(team2Layout);
|
|
|
|
// Entity stats section (right side of info panel)
|
|
QVBoxLayout* entityLayout = new QVBoxLayout();
|
|
m_entityTitleLabel = new QLabel(infoPanel);
|
|
QFont entityTitleFont = m_entityTitleLabel->font();
|
|
entityTitleFont.setBold(true);
|
|
m_entityTitleLabel->setFont(entityTitleFont);
|
|
m_entityTitleLabel->hide();
|
|
entityLayout->addWidget(m_entityTitleLabel);
|
|
|
|
m_entityStatsPanel = new ShipStatsPanel(config, infoPanel);
|
|
m_entityStatsPanel->hide();
|
|
entityLayout->addWidget(m_entityStatsPanel);
|
|
|
|
m_stationStatsLabel = new QLabel(infoPanel);
|
|
m_stationStatsLabel->setWordWrap(true);
|
|
m_stationStatsLabel->hide();
|
|
entityLayout->addWidget(m_stationStatsLabel);
|
|
|
|
entityLayout->addStretch();
|
|
infoLayout->addLayout(entityLayout);
|
|
|
|
mainLayout->addWidget(infoPanel);
|
|
}
|
|
|
|
// Poll timer for info panel updates
|
|
m_pollTimer = new QTimer(this);
|
|
connect(m_pollTimer, &QTimer::timeout, this, &InspectWindow::pollStatus);
|
|
m_pollTimer->start(100);
|
|
|
|
// Show initial status
|
|
pollStatus();
|
|
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
|
|
registerForEvents();
|
|
}
|
|
|
|
InspectWindow::~InspectWindow()
|
|
{
|
|
unregisterForEvents();
|
|
}
|
|
|
|
void InspectWindow::closeEvent(QCloseEvent* event)
|
|
{
|
|
m_arenaView->stopRendering();
|
|
m_pollTimer->stop();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<InspectWindowClosedEvent>());
|
|
event->accept();
|
|
}
|
|
|
|
void InspectWindow::keyPressEvent(QKeyEvent* event)
|
|
{
|
|
if (event->key() == Qt::Key_Space)
|
|
{
|
|
m_arenaView->togglePause();
|
|
}
|
|
else
|
|
{
|
|
QWidget::keyPressEvent(event);
|
|
}
|
|
}
|
|
|
|
void InspectWindow::onSpeedButton(int index)
|
|
{
|
|
if (index >= 0 && index < kSpeedCount)
|
|
{
|
|
m_arenaView->setGameSpeed(kSpeeds[index]);
|
|
}
|
|
}
|
|
|
|
void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
|
{
|
|
for (int i = 0; i < kSpeedCount; ++i)
|
|
{
|
|
const bool active = (std::abs(kSpeeds[i] - event->speed) < 0.001);
|
|
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
|
}
|
|
}
|
|
|
|
void InspectWindow::pollStatus()
|
|
{
|
|
const ArenaStatus status = m_sim->status();
|
|
updateInfoPanel(status);
|
|
refreshEntityStats();
|
|
}
|
|
|
|
void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
|
{
|
|
for (int ti = 0; ti < 2; ++ti)
|
|
{
|
|
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
|
QLabel* header = (ti == 0) ? m_team1Header : m_team2Header;
|
|
QLabel* threat = (ti == 0) ? m_team1Threat : m_team2Threat;
|
|
QLabel* content = (ti == 0) ? m_team1Content : m_team2Content;
|
|
|
|
if (status.finished && status.winnerTeam == ti)
|
|
{
|
|
header->setText(tr("[WON] %1").arg(QString::fromStdString(team.name)));
|
|
}
|
|
else
|
|
{
|
|
header->setText(QString::fromStdString(team.name));
|
|
}
|
|
|
|
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
|
|
|
|
QString lines;
|
|
for (const ArenaStatus::Entry& entry : team.entries)
|
|
{
|
|
if (!lines.isEmpty())
|
|
{
|
|
lines += "\n";
|
|
}
|
|
lines += QString("%1/%2 %3 L%4")
|
|
.arg(entry.surviving)
|
|
.arg(entry.total)
|
|
.arg(QString::fromStdString(entry.displayName))
|
|
.arg(entry.level);
|
|
}
|
|
content->setText(lines);
|
|
}
|
|
}
|
|
|
|
void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event)
|
|
{
|
|
if (event->entity.has_value())
|
|
{
|
|
m_selectedEntity = event->entity;
|
|
|
|
EntityAdmin& admin = m_sim->admin();
|
|
entt::entity entity = *m_selectedEntity;
|
|
|
|
if (!admin.isValid(entity))
|
|
{
|
|
m_selectedEntity = std::nullopt;
|
|
m_entityTitleLabel->hide();
|
|
m_entityStatsPanel->hide();
|
|
m_stationStatsLabel->hide();
|
|
return;
|
|
}
|
|
|
|
if (admin.hasAll<ShipIdentityComponent>(entity))
|
|
{
|
|
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
|
|
m_entityTitleLabel->setText(tr("Ship: %1 (Lv %2)")
|
|
.arg(QString::fromStdString(identity.schematicId))
|
|
.arg(identity.level));
|
|
m_entityTitleLabel->show();
|
|
|
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
|
m_entityStatsPanel->show();
|
|
m_stationStatsLabel->hide();
|
|
}
|
|
else if (admin.hasAll<StationBodyComponent>(entity))
|
|
{
|
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
|
|
m_entityTitleLabel->setText(tr("Defence Station"));
|
|
m_entityTitleLabel->show();
|
|
|
|
float totalDps = 0.0f;
|
|
float maxRange = 0.0f;
|
|
bool hasWeapons = false;
|
|
|
|
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
|
{
|
|
if (owner.owner != entity) { return; }
|
|
hasWeapons = true;
|
|
totalDps += w.damage * w.fireRateHz;
|
|
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
|
});
|
|
|
|
QString statsText = tr("HP: %1 / %2")
|
|
.arg(static_cast<int>(health.hp + 0.5f))
|
|
.arg(static_cast<int>(health.maxHp + 0.5f));
|
|
|
|
if (hasWeapons)
|
|
{
|
|
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
|
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
|
}
|
|
|
|
m_stationStatsLabel->setText(statsText);
|
|
m_stationStatsLabel->show();
|
|
m_entityStatsPanel->hide();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_selectedEntity = std::nullopt;
|
|
m_entityTitleLabel->hide();
|
|
m_entityStatsPanel->hide();
|
|
m_stationStatsLabel->hide();
|
|
}
|
|
}
|
|
|
|
void InspectWindow::refreshEntityStats()
|
|
{
|
|
if (!m_selectedEntity.has_value()) { return; }
|
|
|
|
EntityAdmin& admin = m_sim->admin();
|
|
entt::entity entity = *m_selectedEntity;
|
|
|
|
if (!admin.isValid(entity))
|
|
{
|
|
m_selectedEntity = std::nullopt;
|
|
m_entityTitleLabel->hide();
|
|
m_entityStatsPanel->hide();
|
|
m_stationStatsLabel->hide();
|
|
return;
|
|
}
|
|
|
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
if (health.hp <= 0.0f)
|
|
{
|
|
m_selectedEntity = std::nullopt;
|
|
m_entityTitleLabel->hide();
|
|
m_entityStatsPanel->hide();
|
|
m_stationStatsLabel->hide();
|
|
return;
|
|
}
|
|
|
|
if (admin.hasAll<ShipIdentityComponent>(entity))
|
|
{
|
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
|
}
|
|
else if (admin.hasAll<StationBodyComponent>(entity))
|
|
{
|
|
float totalDps = 0.0f;
|
|
float maxRange = 0.0f;
|
|
bool hasWeapons = false;
|
|
|
|
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
|
{
|
|
if (owner.owner != entity) { return; }
|
|
hasWeapons = true;
|
|
totalDps += w.damage * w.fireRateHz;
|
|
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
|
});
|
|
|
|
QString statsText = tr("HP: %1 / %2")
|
|
.arg(static_cast<int>(health.hp + 0.5f))
|
|
.arg(static_cast<int>(health.maxHp + 0.5f));
|
|
|
|
if (hasWeapons)
|
|
{
|
|
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
|
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
|
}
|
|
|
|
m_stationStatsLabel->setText(statsText);
|
|
}
|
|
}
|