#include "InspectWindow.h" #include #include #include #include #include #include #include "ArenaView.h" #include "EntityAdmin.h" #include "EventManager.h" #include "HealthComponent.h" #include "InspectWindowClosedEvent.h" #include "ModuleOwnerComponent.h" #include "SelectedBehaviorComponent.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); m_durationLabel = new QLabel(header); headerLayout->addWidget(m_durationLabel); 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(&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_team1Ehp = new QLabel(infoPanel); team1Layout->addWidget(m_team1Ehp); 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_team2Ehp = new QLabel(infoPanel); team2Layout->addWidget(m_team2Ehp); 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()); 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 event) { for (int i = 0; i < kSpeedCount; ++i) { const bool active = (std::abs(kSpeeds[i] - event->speed) < 0.001); m_speedButtons[static_cast(i)]->setChecked(active); } } void InspectWindow::pollStatus() { const ArenaStatus status = m_sim->getStatus(); updateInfoPanel(status); refreshEntityStats(); } void InspectWindow::updateInfoPanel(const ArenaStatus& status) { m_durationLabel->setText(status.finished ? tr("Duration: %1 s").arg(QString::number(status.durationSeconds, 'f', 1)) : QString()); 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* ehp = (ti == 0) ? m_team1Ehp : m_team2Ehp; 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))); ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.getEhpPercentText()))); QString lines; for (const ArenaStatus::Entry& entry : team.entries) { if (!lines.isEmpty()) { lines += "\n"; } lines += QString("%1/%2 %3") .arg(entry.surviving) .arg(entry.total) .arg(QString::fromStdString(entry.displayName)); if (entry.level.has_value()) { lines += QString(" L%1").arg(entry.level.value()); } } content->setText(lines); } } void InspectWindow::handleEvent(std::shared_ptr event) { if (!event->entities.empty()) { // The arena is single-select, so only the first entity is inspected. m_selectedEntity = event->entities.front(); EntityAdmin& admin = m_sim->getAdmin(); 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(entity)) { const ShipIdentityComponent& identity = admin.get(entity); const HealthComponent& health = admin.get(entity); m_entityTitleLabel->setText(tr("Ship: %1") .arg(QString::fromStdString(identity.schematicId))); m_entityTitleLabel->show(); const ShipStats stats = buildShipStatsFromEntity(admin, entity); m_entityStatsPanel->refreshFromLive(stats, health.hp); m_entityStatsPanel->setBehavior( admin.get(entity).winner); m_entityStatsPanel->show(); m_stationStatsLabel->hide(); } else if (admin.hasAll(entity)) { const HealthComponent& health = admin.get(entity); m_entityTitleLabel->setText(tr("Defence Station")); m_entityTitleLabel->show(); float totalDps = 0.0f; float maxRange = 0.0f; bool hasWeapons = false; admin.forEach( [&](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(health.hp + 0.5f)) .arg(static_cast(health.maxHp + 0.5f)); if (hasWeapons) { statsText += tr("\nDPS: %1").arg(QString::number(static_cast(totalDps), 'f', 1)); statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast(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->getAdmin(); 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(entity); if (health.hp <= 0.0f) { m_selectedEntity = std::nullopt; m_entityTitleLabel->hide(); m_entityStatsPanel->hide(); m_stationStatsLabel->hide(); return; } if (admin.hasAll(entity)) { const ShipStats stats = buildShipStatsFromEntity(admin, entity); m_entityStatsPanel->refreshFromLive(stats, health.hp); m_entityStatsPanel->setBehavior( admin.get(entity).winner); } else if (admin.hasAll(entity)) { float totalDps = 0.0f; float maxRange = 0.0f; bool hasWeapons = false; admin.forEach( [&](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(health.hp + 0.5f)) .arg(static_cast(health.maxHp + 0.5f)); if (hasWeapons) { statsText += tr("\nDPS: %1").arg(QString::number(static_cast(totalDps), 'f', 1)); statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast(maxRange), 'f', 1)); } m_stationStatsLabel->setText(statsText); } }