Files
dota_factory/src/balancing/InspectWindow.cpp

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);
}
}