add live ship stats panel

This commit is contained in:
2026-06-07 21:07:19 +02:00
parent 37a70ea321
commit f097e9a25f
20 changed files with 723 additions and 45 deletions

View File

@@ -4,12 +4,16 @@
#include <cmath>
#include <optional>
#include <QMouseEvent>
#include <QPainter>
#include <QPoint>
#include "ArenaSimulation.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "EntityHitTest.h"
#include "EntitySelectedEvent.h"
#include "EventManager.h"
#include "FacingComponent.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
@@ -198,6 +202,37 @@ std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
return m_sim->admin().get<PositionComponent>(entity).value;
}
QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const
{
const float px = tilePx();
if (px < 0.001f) { return QVector2D(0.0f, 0.0f); }
return QVector2D(static_cast<float>(widgetPt.x()) / px,
static_cast<float>(widgetPt.y()) / px);
}
void ArenaView::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
{
const QVector2D worldPos = widgetToWorld(event->pos());
entt::entity hit = entityAtWorldPos(m_sim->admin(), worldPos);
if (hit != entt::null)
{
m_selectedEntity = hit;
}
else
{
m_selectedEntity = std::nullopt;
}
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(m_selectedEntity));
}
QOpenGLWidget::mousePressEvent(event);
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
@@ -268,7 +303,7 @@ void ArenaView::drawScrap(QPainter& painter)
void ArenaView::drawStations(QPainter& painter)
{
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
{
const BuildingType visType = f.isEnemy
? BuildingType::EnemyDefenceStation
@@ -304,6 +339,13 @@ void ArenaView::drawStations(QPainter& painter)
painter.fillRect(QRectF(bboxRect.left(), barY, barW * static_cast<qreal>(fraction), barH),
f.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
}
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
{
painter.setPen(QPen(QColor(255, 255, 0), 2));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect.adjusted(-2, -2, 2, 2));
}
});
}
@@ -311,7 +353,7 @@ void ArenaView::drawShips(QPainter& painter)
{
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
[&](entt::entity e, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& facing,
const FactionComponent& fac, const HealthComponent& h)
{
@@ -349,6 +391,14 @@ void ArenaView::drawShips(QPainter& painter)
painter.fillRect(QRectF(barX, barY, barW * static_cast<qreal>(fraction), barH),
fac.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
}
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
{
const qreal radius = static_cast<qreal>(tilePx()) * 0.55;
painter.setPen(QPen(QColor(255, 255, 0), 2));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(center, radius, radius);
}
});
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <optional>
#include <random>
#include <vector>
@@ -11,6 +12,7 @@
#include "FireEvent.h"
#include "entt/entity/entity.hpp"
#include "EntitySelectedEvent.h"
#include "Tick.h"
#include "TickDriver.h"
#include "VisualsConfig.h"
@@ -37,6 +39,7 @@ signals:
protected:
void paintGL() override;
void mousePressEvent(QMouseEvent* event) override;
private slots:
void onFrame();
@@ -55,6 +58,7 @@ private:
QRectF tileRect(QPoint tile) const;
std::optional<QVector2D> entityPosition(entt::entity entity) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
struct ActiveBeam
{
@@ -79,4 +83,6 @@ private:
std::vector<ActiveBeam> m_activeBeams;
bool m_finishedEmitted;
std::optional<entt::entity> m_selectedEntity;
};

View File

@@ -209,7 +209,7 @@ void BalancingWindow::inspectArena(int index)
entry.widget->updateStatus(m_inspectedSim->status());
m_inspectWindow = new InspectWindow(
m_inspectedSim.get(), &m_visuals, entry.config.name, nullptr);
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
connect(m_inspectWindow, &InspectWindow::closed,
this, &BalancingWindow::closeInspectWindow);

View File

@@ -6,6 +6,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.h
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.h
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
PARENT_SCOPE
@@ -20,6 +21,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
PARENT_SCOPE
)

View File

@@ -9,14 +9,24 @@
#include <QVBoxLayout>
#include "ArenaView.h"
#include "EntityAdmin.h"
#include "HealthComponent.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 VisualsConfig* visuals,
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);
@@ -96,6 +106,27 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
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);
}
@@ -108,6 +139,13 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
pollStatus();
setFocusPolicy(Qt::StrongFocus);
registerForEvent();
}
InspectWindow::~InspectWindow()
{
unregisterForEvent();
}
void InspectWindow::closeEvent(QCloseEvent* event)
@@ -151,6 +189,7 @@ void InspectWindow::pollStatus()
{
const ArenaStatus status = m_sim->status();
updateInfoPanel(status);
refreshEntityStats();
}
void InspectWindow::updateInfoPanel(const ArenaStatus& status)
@@ -186,3 +225,140 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
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);
}
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
@@ -8,18 +9,27 @@
#include <QTimer>
#include <QWidget>
#include "entt/entity/entity.hpp"
#include "ArenaSimulation.h"
#include "EntitySelectedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
#include "VisualsConfig.h"
class ArenaView;
class ShipStatsPanel;
class InspectWindow : public QWidget
class InspectWindow : public QWidget,
public EventHandler<EntitySelectedEvent>
{
Q_OBJECT
public:
InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
InspectWindow(ArenaSimulation* sim, const GameConfig* config,
const VisualsConfig* visuals,
const std::string& arenaName, QWidget* parent = nullptr);
~InspectWindow() override;
signals:
void closed();
@@ -28,6 +38,9 @@ protected:
void closeEvent(QCloseEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
private:
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
private slots:
void onSpeedButton(int index);
void onSpeedChanged(double multiplier);
@@ -35,9 +48,11 @@ private slots:
private:
void updateInfoPanel(const ArenaStatus& status);
void refreshEntityStats();
ArenaSimulation* m_sim;
ArenaView* m_arenaView;
ArenaSimulation* m_sim;
const GameConfig* m_config;
ArenaView* m_arenaView;
std::vector<QPushButton*> m_speedButtons;
QLabel* m_team1Header;
@@ -46,6 +61,11 @@ private:
QLabel* m_team2Content;
QTimer* m_pollTimer;
std::optional<entt::entity> m_selectedEntity;
QLabel* m_entityTitleLabel;
ShipStatsPanel* m_entityStatsPanel;
QLabel* m_stationStatsLabel;
static const double kSpeeds[];
static const int kSpeedCount;
};

View File

@@ -3,6 +3,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
PARENT_SCOPE

View File

@@ -0,0 +1,21 @@
#ifndef ENTITY_SELECTED_EVENT_H
#define ENTITY_SELECTED_EVENT_H
#include <optional>
#include "entt/entity/entity.hpp"
#include "Event.h"
class EntitySelectedEvent : public Event
{
public:
explicit EntitySelectedEvent(std::optional<entt::entity> entity)
: entity(entity)
{
}
const std::optional<entt::entity> entity;
};
#endif // ENTITY_SELECTED_EVENT_H

View File

@@ -5,6 +5,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
@@ -18,6 +19,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
PARENT_SCOPE

View File

@@ -0,0 +1,56 @@
#include "EntityHitTest.h"
#include <cmath>
#include "EntityAdmin.h"
#include "PositionComponent.h"
#include "StationBodyComponent.h"
#include "HealthComponent.h"
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
{
const QPoint tile(static_cast<int>(std::floor(worldPos.x())),
static_cast<int>(std::floor(worldPos.y())));
entt::entity stationHit = entt::null;
admin.forEach<StationBodyComponent, HealthComponent>(
[&](entt::entity entity, const StationBodyComponent& sb, const HealthComponent& h)
{
if (stationHit != entt::null) { return; }
if (h.hp <= 0.0f) { return; }
for (const QPoint& cell : sb.bodyCells)
{
if (cell == tile)
{
stationHit = entity;
return;
}
}
});
if (stationHit != entt::null)
{
return stationHit;
}
constexpr float kShipHitRadiusSquared = 0.5f * 0.5f;
entt::entity bestShip = entt::null;
float bestDistSquared = kShipHitRadiusSquared;
admin.forEach<PositionComponent, HealthComponent>(
[&](entt::entity entity, const PositionComponent& pos, const HealthComponent& h)
{
if (h.hp <= 0.0f) { return; }
if (admin.hasAll<StationBodyComponent>(entity)) { return; }
const float dx = pos.value.x() - worldPos.x();
const float dy = pos.value.y() - worldPos.y();
const float distSquared = dx * dx + dy * dy;
if (distSquared < bestDistSquared)
{
bestDistSquared = distSquared;
bestShip = entity;
}
});
return bestShip;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include <QVector2D>
#include "entt/entity/entity.hpp"
class EntityAdmin;
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);

View File

@@ -3,6 +3,16 @@
#include <map>
#include <utility>
#include "DynamicBodyComponent.h"
#include "EntityAdmin.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "RepairToolComponent.h"
#include "SalvageCargoComponent.h"
#include "SensorRangeComponent.h"
#include "Tick.h"
#include "WeaponComponent.h"
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
@@ -216,3 +226,68 @@ ShipStats calculateShipStats(const GameConfig& config,
return result;
}
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity)
{
ShipStats result{};
const HealthComponent& health = admin.get<HealthComponent>(shipEntity);
const DynamicBodyComponent& body = admin.get<DynamicBodyComponent>(shipEntity);
const SensorRangeComponent& sensor = admin.get<SensorRangeComponent>(shipEntity);
result.hp = health.maxHp;
result.maxSpeed_tps = body.maxSpeed_tpt * kTickRateHz;
result.sensorRange_tiles = sensor.value_tiles;
result.mainAcceleration_tpss = body.mainAcceleration_tptt * kTickRateHz * kTickRateHz;
result.maneuveringAcceleration_tpss = body.maneuveringAcceleration_tptt * kTickRateHz * kTickRateHz;
result.angularAcceleration_radpss = body.maxAngularAcceleration_rptt * kTickRateHz * kTickRateHz;
result.maxRotationSpeed_radps = body.maxRotationSpeed_rpt * kTickRateHz;
float weaponDps = 0.0f;
float weaponMaxRange = 0.0f;
bool hasWeapons = false;
float salvageRate = 0.0f;
float salvageMaxRange = 0.0f;
bool hasSalvage = false;
float repairRate = 0.0f;
float repairMaxRange = 0.0f;
bool hasRepair = false;
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
{
if (owner.owner != shipEntity) { return; }
hasWeapons = true;
weaponDps += w.damage * w.fireRateHz;
if (w.range_tiles > weaponMaxRange) { weaponMaxRange = w.range_tiles; }
});
admin.forEach<ModuleOwnerComponent, SalvageCargoComponent>(
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const SalvageCargoComponent& s)
{
if (owner.owner != shipEntity) { return; }
hasSalvage = true;
const float rate = (s.collectionIntervalTicks > 0)
? static_cast<float>(kTickRateHz) / static_cast<float>(s.collectionIntervalTicks)
: 0.0f;
salvageRate += rate;
if (s.collectionRange_tiles > salvageMaxRange) { salvageMaxRange = s.collectionRange_tiles; }
});
admin.forEach<ModuleOwnerComponent, RepairToolComponent>(
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const RepairToolComponent& r)
{
if (owner.owner != shipEntity) { return; }
hasRepair = true;
repairRate += r.ratePerTick * kTickRateHz;
if (r.range_tiles > repairMaxRange) { repairMaxRange = r.range_tiles; }
});
if (hasWeapons) { result.weapons = ShipStats::WeaponStats{weaponDps, weaponMaxRange}; }
if (hasSalvage) { result.salvage = ShipStats::SalvageStats{salvageRate, salvageMaxRange}; }
if (hasRepair) { result.repair = ShipStats::RepairStats{repairRate, repairMaxRange}; }
return result;
}

View File

@@ -4,9 +4,13 @@
#include <string>
#include <vector>
#include "entt/entity/entity.hpp"
#include "GameConfig.h"
#include "ShipLayout.h"
class EntityAdmin;
// Effective stats for a ship with a given layout, after applying all passive
// module modifiers per REQ-MOD-STAT-CALC. Values are in display units:
// speeds in tiles/s, ranges in tiles, accelerations in tiles/s² or rad/s².
@@ -47,3 +51,6 @@ ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules);
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);

View File

@@ -20,6 +20,8 @@
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "EntityHitTest.h"
#include "EntitySelectedEvent.h"
#include "EventManager.h"
#include "FacingComponent.h"
#include "FactionComponent.h"
@@ -338,6 +340,13 @@ QPoint GameWorldView::widgetToTile(QPoint widgetPt) const
return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy)));
}
QVector2D GameWorldView::widgetToWorld(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + m_scrollXTiles;
const float wy = static_cast<float>(widgetPt.y()) / tilePx();
return QVector2D(wx, wy);
}
QRectF GameWorldView::tileRect(QPoint tile) const
{
const QPointF tl = tileToWidget(tile);
@@ -834,7 +843,7 @@ void GameWorldView::drawScrap(QPainter& painter)
void GameWorldView::drawStations(QPainter& painter)
{
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f,
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f,
const HealthComponent& h)
{
const BuildingType visType = f.isEnemy
@@ -860,6 +869,13 @@ void GameWorldView::drawStations(QPainter& painter)
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect);
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
{
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect.adjusted(-2, -2, 2, 2));
}
// HP bar below footprint.
if (h.maxHp > 0.0f)
{
@@ -879,7 +895,7 @@ void GameWorldView::drawShips(QPainter& painter)
{
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
[&](entt::entity e, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& facing,
const FactionComponent& fac, const HealthComponent& h)
{
@@ -906,6 +922,14 @@ void GameWorldView::drawShips(QPainter& painter)
painter.setBrush(it->second.fill);
painter.drawPolygon(tri);
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
{
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush);
const qreal r = static_cast<qreal>(fwd) + 2.0;
painter.drawEllipse(center, r, r);
}
if (h.maxHp > 0.0f)
{
const float fraction = std::max(0.0f, h.hp / h.maxHp);
@@ -1271,41 +1295,62 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
}
else
{
BuildingId id = buildingAtTile(tile);
if (id == kInvalidBuildingId)
const QVector2D worldPos = widgetToWorld(event->pos());
const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos);
if (hitEntity != entt::null)
{
id = siteAtTile(tile);
}
if (id != kInvalidBuildingId)
{
if (event->modifiers() & Qt::ControlModifier)
{
bool found = false;
std::vector<BuildingId> newSel;
for (BuildingId sel : m_selectedBuildingIds)
{
if (sel == id) { found = true; }
else { newSel.push_back(sel); }
}
if (!found) { newSel.push_back(id); }
m_selectedBuildingIds = newSel;
}
else
{
m_selectedBuildingIds = { id };
}
m_selectedBuildingIds.clear();
emit selectionChanged(m_selectedBuildingIds);
m_selectedEntity = hitEntity;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(hitEntity));
}
else
{
if (!(event->modifiers() & Qt::ControlModifier))
if (m_selectedEntity.has_value())
{
m_selectedBuildingIds.clear();
m_selectedEntity = std::nullopt;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(std::nullopt));
}
BuildingId id = buildingAtTile(tile);
if (id == kInvalidBuildingId)
{
id = siteAtTile(tile);
}
if (id != kInvalidBuildingId)
{
if (event->modifiers() & Qt::ControlModifier)
{
bool found = false;
std::vector<BuildingId> newSel;
for (BuildingId sel : m_selectedBuildingIds)
{
if (sel == id) { found = true; }
else { newSel.push_back(sel); }
}
if (!found) { newSel.push_back(id); }
m_selectedBuildingIds = newSel;
}
else
{
m_selectedBuildingIds = { id };
}
emit selectionChanged(m_selectedBuildingIds);
}
m_boxSelecting = true;
m_boxStartTile = tile;
m_boxCurrentTile = tile;
else
{
if (!(event->modifiers() & Qt::ControlModifier))
{
m_selectedBuildingIds.clear();
emit selectionChanged(m_selectedBuildingIds);
}
m_boxSelecting = true;
m_boxStartTile = tile;
m_boxCurrentTile = tile;
}
}
}
}

View File

@@ -19,6 +19,7 @@
#include "FireEvent.h"
#include "entt/entity/entity.hpp"
#include "EntitySelectedEvent.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "Tick.h"
@@ -106,6 +107,7 @@ private:
const BuildingDef* findBuildingDef(BuildingType type) const;
BuildingId buildingAtTile(QPoint tile) const;
BuildingId siteAtTile(QPoint tile) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color);
@@ -166,6 +168,7 @@ private:
bool m_debugDraw;
std::vector<BuildingId> m_selectedBuildingIds;
std::optional<entt::entity> m_selectedEntity;
bool m_boxSelecting;
QPoint m_boxStartTile;
QPoint m_boxCurrentTile;

View File

@@ -13,6 +13,16 @@
#include <QVBoxLayout>
#include "BeltSystem.h"
#include "DynamicBodyComponent.h"
#include "EntityAdmin.h"
#include "EntitySelectedEvent.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "ShipIdentityComponent.h"
#include "ShipStatsCalculator.h"
#include "ShipStatsPanel.h"
#include "StationBodyComponent.h"
#include "TickAdvancedEvent.h"
#include "Building.h"
#include "BuildingSystem.h"
@@ -22,6 +32,7 @@
#include "Rotation.h"
#include "ShipLayoutPreview.h"
#include "Simulation.h"
#include "WeaponComponent.h"
namespace
{
@@ -136,19 +147,39 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
connect(m_filterBList, &QListWidget::itemChanged,
this, &SelectedBuildingPanel::onSplitterFilterChanged);
m_entityTitleLabel = new QLabel(this);
QFont titleFont = m_entityTitleLabel->font();
titleFont.setBold(true);
m_entityTitleLabel->setFont(titleFont);
m_layout->addWidget(m_entityTitleLabel);
m_entityTitleLabel->hide();
m_entityStatsPanel = new ShipStatsPanel(config, this);
m_layout->addWidget(m_entityStatsPanel);
m_entityStatsPanel->hide();
m_stationStatsLabel = new QLabel(this);
m_stationStatsLabel->setWordWrap(true);
m_layout->addWidget(m_stationStatsLabel);
m_stationStatsLabel->hide();
buildEmpty();
registerForEvent();
registerForEvents();
}
SelectedBuildingPanel::~SelectedBuildingPanel()
{
unregisterForEvent();
unregisterForEvents();
}
void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
{
m_selectedBuildingIds = ids;
if (!ids.empty())
{
clearEntityDisplay();
}
rebuild();
}
@@ -168,7 +199,7 @@ void SelectedBuildingPanel::rebuild()
}
}
void SelectedBuildingPanel::buildEmpty()
void SelectedBuildingPanel::clearContent()
{
m_singleBuildingId = kInvalidBuildingId;
m_titleLabel->hide();
@@ -183,6 +214,14 @@ void SelectedBuildingPanel::buildEmpty()
m_buffersLabel->hide();
}
void SelectedBuildingPanel::buildEmpty()
{
clearContent();
m_entityTitleLabel->hide();
m_entityStatsPanel->hide();
m_stationStatsLabel->hide();
}
void SelectedBuildingPanel::buildSingle(BuildingId id)
{
m_singleBuildingId = id;
@@ -498,12 +537,16 @@ const ShipDef* SelectedBuildingPanel::findShipDef(const std::string& id) const
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
{
if (m_selectedEntity.has_value())
{
refreshEntityStats();
return;
}
if (m_singleBuildingId == kInvalidBuildingId) { return; }
const Building* b = m_sim->buildings().findBuilding(m_singleBuildingId);
if (b)
{
// If the panel was last showing this id as a construction site, the
// full building UI (recipe combo, ports, etc.) hasn't been built yet.
if (m_titleLabel->text().startsWith(tr("(Building) ")))
{
rebuild();
@@ -688,3 +731,129 @@ void SelectedBuildingPanel::onClearBelt()
m_sim->belts().clearTiles(tiles);
}
}
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEvent> event)
{
if (event->entity.has_value())
{
m_selectedEntity = event->entity;
m_selectedBuildingIds.clear();
clearContent();
EntityAdmin& admin = m_sim->admin();
entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity))
{
clearEntityDisplay();
return;
}
if (admin.hasAll<ShipIdentityComponent>(entity))
{
buildEntityShip(entity);
}
else if (admin.hasAll<StationBodyComponent>(entity))
{
buildEntityStation(entity);
}
}
else
{
clearEntityDisplay();
}
}
void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
{
EntityAdmin& admin = m_sim->admin();
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();
}
void SelectedBuildingPanel::buildEntityStation(entt::entity entity)
{
EntityAdmin& admin = m_sim->admin();
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();
}
void SelectedBuildingPanel::refreshEntityStats()
{
if (!m_selectedEntity.has_value()) { return; }
EntityAdmin& admin = m_sim->admin();
entt::entity entity = *m_selectedEntity;
if (!admin.isValid(entity))
{
clearEntityDisplay();
return;
}
const HealthComponent& health = admin.get<HealthComponent>(entity);
if (health.hp <= 0.0f)
{
clearEntityDisplay();
return;
}
if (admin.hasAll<ShipIdentityComponent>(entity))
{
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
m_entityStatsPanel->refreshFromLive(stats, health.hp);
}
else if (admin.hasAll<StationBodyComponent>(entity))
{
buildEntityStation(entity);
}
}
void SelectedBuildingPanel::clearEntityDisplay()
{
m_selectedEntity = std::nullopt;
m_entityTitleLabel->hide();
m_entityStatsPanel->hide();
m_stationStatsLabel->hide();
}

View File

@@ -1,13 +1,17 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include <QPoint>
#include <QWidget>
#include "entt/entity/entity.hpp"
#include "Building.h"
#include "BuildingId.h"
#include "EntitySelectedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
#include "RecipesConfig.h"
@@ -18,6 +22,7 @@
class Simulation;
class ShipLayoutPreview;
class ShipStatsPanel;
class QLabel;
class QComboBox;
class QListWidget;
@@ -25,7 +30,7 @@ class QPushButton;
class QVBoxLayout;
class SelectedBuildingPanel : public QWidget,
public EventHandler<TickAdvancedEvent>
public CombinedEventHandler<TickAdvancedEvent, EntitySelectedEvent>
{
Q_OBJECT
@@ -42,6 +47,7 @@ public slots:
private:
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
private slots:
void onRecipeChanged(int comboIndex);
@@ -80,4 +86,14 @@ private:
BuildingId m_singleBuildingId;
QPoint m_splitterTile;
std::string m_currentRecipeId;
std::optional<entt::entity> m_selectedEntity;
ShipStatsPanel* m_entityStatsPanel;
QLabel* m_entityTitleLabel;
QLabel* m_stationStatsLabel;
void buildEntityShip(entt::entity entity);
void buildEntityStation(entt::entity entity);
void refreshEntityStats();
void clearEntityDisplay();
};

View File

@@ -111,9 +111,21 @@ void ShipStatsPanel::refresh(const std::string& shipId,
const std::vector<PlacedModule>& modules)
{
const ShipStats stats = calculateShipStats(*m_config, shipId, level, modules);
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
applyStats(stats, hpText);
}
m_hpLabel->setText(
tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f)));
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
{
const QString hpText = tr("HP: %1 / %2")
.arg(static_cast<int>(currentHp + 0.5f))
.arg(static_cast<int>(stats.hp + 0.5f));
applyStats(stats, hpText);
}
void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText)
{
m_hpLabel->setText(hpText);
m_speedLabel->setText(
tr("Max Speed: %1 tiles/s").arg(fmt(stats.maxSpeed_tps)));
m_sensorRangeLabel->setText(

View File

@@ -6,6 +6,7 @@
#include <QWidget>
#include "ShipLayout.h"
#include "ShipStatsCalculator.h"
struct GameConfig;
class QLabel;
@@ -21,7 +22,11 @@ public:
int level,
const std::vector<PlacedModule>& modules);
void refreshFromLive(const ShipStats& stats, float currentHp);
private:
void applyStats(const ShipStats& stats, const QString& hpText);
const GameConfig* m_config;
QLabel* m_hpLabel;