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

@@ -366,6 +366,9 @@ The screen is divided into three vertical sections:
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. - REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown.
- REQ-UI-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic dropdown (REQ-MOD-UI-PREVIEW). - REQ-UI-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic dropdown (REQ-MOD-UI-PREVIEW).
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. - REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection.
- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed at the ship's actual level with its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed.
- REQ-UI-STATION-STATS-PANEL: When a single defence station is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate.
### Build Button Grid ### Build Button Grid

View File

@@ -4,12 +4,16 @@
#include <cmath> #include <cmath>
#include <optional> #include <optional>
#include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPoint> #include <QPoint>
#include "ArenaSimulation.h" #include "ArenaSimulation.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "EntityHitTest.h"
#include "EntitySelectedEvent.h"
#include "EventManager.h"
#include "FacingComponent.h" #include "FacingComponent.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "HealthComponent.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; 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 // Rendering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -268,7 +303,7 @@ void ArenaView::drawScrap(QPainter& painter)
void ArenaView::drawStations(QPainter& painter) void ArenaView::drawStations(QPainter& painter)
{ {
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( 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 const BuildingType visType = f.isEnemy
? BuildingType::EnemyDefenceStation ? BuildingType::EnemyDefenceStation
@@ -304,6 +339,13 @@ void ArenaView::drawStations(QPainter& painter)
painter.fillRect(QRectF(bboxRect.left(), barY, barW * static_cast<qreal>(fraction), barH), painter.fillRect(QRectF(bboxRect.left(), barY, barW * static_cast<qreal>(fraction), barH),
f.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60)); 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, m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>( FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity e, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& facing, const PositionComponent& pos, const FacingComponent& facing,
const FactionComponent& fac, const HealthComponent& h) 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), painter.fillRect(QRectF(barX, barY, barW * static_cast<qreal>(fraction), barH),
fac.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60)); 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 #pragma once
#include <optional>
#include <random> #include <random>
#include <vector> #include <vector>
@@ -11,6 +12,7 @@
#include "FireEvent.h" #include "FireEvent.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
#include "EntitySelectedEvent.h"
#include "Tick.h" #include "Tick.h"
#include "TickDriver.h" #include "TickDriver.h"
#include "VisualsConfig.h" #include "VisualsConfig.h"
@@ -37,6 +39,7 @@ signals:
protected: protected:
void paintGL() override; void paintGL() override;
void mousePressEvent(QMouseEvent* event) override;
private slots: private slots:
void onFrame(); void onFrame();
@@ -55,6 +58,7 @@ private:
QRectF tileRect(QPoint tile) const; QRectF tileRect(QPoint tile) const;
std::optional<QVector2D> entityPosition(entt::entity entity) const; std::optional<QVector2D> entityPosition(entt::entity entity) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
struct ActiveBeam struct ActiveBeam
{ {
@@ -79,4 +83,6 @@ private:
std::vector<ActiveBeam> m_activeBeams; std::vector<ActiveBeam> m_activeBeams;
bool m_finishedEmitted; 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()); entry.widget->updateStatus(m_inspectedSim->status());
m_inspectWindow = new InspectWindow( 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, connect(m_inspectWindow, &InspectWindow::closed,
this, &BalancingWindow::closeInspectWindow); this, &BalancingWindow::closeInspectWindow);

View File

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

View File

@@ -9,14 +9,24 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include "ArenaView.h" #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 double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
const int InspectWindow::kSpeedCount = 5; 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) const std::string& arenaName, QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_sim(sim) , m_sim(sim)
, m_config(config)
{ {
setWindowTitle(tr("Inspect \u2014 %1").arg(QString::fromStdString(arenaName))); setWindowTitle(tr("Inspect \u2014 %1").arg(QString::fromStdString(arenaName)));
resize(900, 700); resize(900, 700);
@@ -96,6 +106,27 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
team2Layout->addStretch(); team2Layout->addStretch();
infoLayout->addLayout(team2Layout); 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); mainLayout->addWidget(infoPanel);
} }
@@ -108,6 +139,13 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
pollStatus(); pollStatus();
setFocusPolicy(Qt::StrongFocus); setFocusPolicy(Qt::StrongFocus);
registerForEvent();
}
InspectWindow::~InspectWindow()
{
unregisterForEvent();
} }
void InspectWindow::closeEvent(QCloseEvent* event) void InspectWindow::closeEvent(QCloseEvent* event)
@@ -151,6 +189,7 @@ void InspectWindow::pollStatus()
{ {
const ArenaStatus status = m_sim->status(); const ArenaStatus status = m_sim->status();
updateInfoPanel(status); updateInfoPanel(status);
refreshEntityStats();
} }
void InspectWindow::updateInfoPanel(const ArenaStatus& status) void InspectWindow::updateInfoPanel(const ArenaStatus& status)
@@ -186,3 +225,140 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
content->setText(lines); 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 #pragma once
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -8,18 +9,27 @@
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
#include "entt/entity/entity.hpp"
#include "ArenaSimulation.h" #include "ArenaSimulation.h"
#include "EntitySelectedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
#include "VisualsConfig.h" #include "VisualsConfig.h"
class ArenaView; class ArenaView;
class ShipStatsPanel;
class InspectWindow : public QWidget class InspectWindow : public QWidget,
public EventHandler<EntitySelectedEvent>
{ {
Q_OBJECT Q_OBJECT
public: public:
InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals, InspectWindow(ArenaSimulation* sim, const GameConfig* config,
const VisualsConfig* visuals,
const std::string& arenaName, QWidget* parent = nullptr); const std::string& arenaName, QWidget* parent = nullptr);
~InspectWindow() override;
signals: signals:
void closed(); void closed();
@@ -28,6 +38,9 @@ protected:
void closeEvent(QCloseEvent* event) override; void closeEvent(QCloseEvent* event) override;
void keyPressEvent(QKeyEvent* event) override; void keyPressEvent(QKeyEvent* event) override;
private:
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
private slots: private slots:
void onSpeedButton(int index); void onSpeedButton(int index);
void onSpeedChanged(double multiplier); void onSpeedChanged(double multiplier);
@@ -35,9 +48,11 @@ private slots:
private: private:
void updateInfoPanel(const ArenaStatus& status); void updateInfoPanel(const ArenaStatus& status);
void refreshEntityStats();
ArenaSimulation* m_sim; ArenaSimulation* m_sim;
ArenaView* m_arenaView; const GameConfig* m_config;
ArenaView* m_arenaView;
std::vector<QPushButton*> m_speedButtons; std::vector<QPushButton*> m_speedButtons;
QLabel* m_team1Header; QLabel* m_team1Header;
@@ -46,6 +61,11 @@ private:
QLabel* m_team2Content; QLabel* m_team2Content;
QTimer* m_pollTimer; QTimer* m_pollTimer;
std::optional<entt::entity> m_selectedEntity;
QLabel* m_entityTitleLabel;
ShipStatsPanel* m_entityStatsPanel;
QLabel* m_stationStatsLabel;
static const double kSpeeds[]; static const double kSpeeds[];
static const int kSpeedCount; static const int kSpeedCount;
}; };

View File

@@ -3,6 +3,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
PARENT_SCOPE 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}/BeltSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Building.h ${CMAKE_CURRENT_SOURCE_DIR}/Building.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
@@ -18,6 +19,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
PARENT_SCOPE 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 <map>
#include <utility> #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, ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId, const std::string& shipId,
int level, int level,
@@ -216,3 +226,68 @@ ShipStats calculateShipStats(const GameConfig& config,
return result; 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 <string>
#include <vector> #include <vector>
#include "entt/entity/entity.hpp"
#include "GameConfig.h" #include "GameConfig.h"
#include "ShipLayout.h" #include "ShipLayout.h"
class EntityAdmin;
// Effective stats for a ship with a given layout, after applying all passive // 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: // 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². // 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, const std::string& shipId,
int level, int level,
const std::vector<PlacedModule>& modules); const std::vector<PlacedModule>& modules);
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);

View File

@@ -20,6 +20,8 @@
#include "BeltSystem.h" #include "BeltSystem.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "EntityHitTest.h"
#include "EntitySelectedEvent.h"
#include "EventManager.h" #include "EventManager.h"
#include "FacingComponent.h" #include "FacingComponent.h"
#include "FactionComponent.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))); 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 QRectF GameWorldView::tileRect(QPoint tile) const
{ {
const QPointF tl = tileToWidget(tile); const QPointF tl = tileToWidget(tile);
@@ -834,7 +843,7 @@ void GameWorldView::drawScrap(QPainter& painter)
void GameWorldView::drawStations(QPainter& painter) void GameWorldView::drawStations(QPainter& painter)
{ {
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( 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 HealthComponent& h)
{ {
const BuildingType visType = f.isEnemy const BuildingType visType = f.isEnemy
@@ -860,6 +869,13 @@ void GameWorldView::drawStations(QPainter& painter)
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect); 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. // HP bar below footprint.
if (h.maxHp > 0.0f) if (h.maxHp > 0.0f)
{ {
@@ -879,7 +895,7 @@ void GameWorldView::drawShips(QPainter& painter)
{ {
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent, m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>( FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si, [&](entt::entity e, const ShipIdentityComponent& si,
const PositionComponent& pos, const FacingComponent& facing, const PositionComponent& pos, const FacingComponent& facing,
const FactionComponent& fac, const HealthComponent& h) const FactionComponent& fac, const HealthComponent& h)
{ {
@@ -906,6 +922,14 @@ void GameWorldView::drawShips(QPainter& painter)
painter.setBrush(it->second.fill); painter.setBrush(it->second.fill);
painter.drawPolygon(tri); 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) if (h.maxHp > 0.0f)
{ {
const float fraction = std::max(0.0f, h.hp / h.maxHp); const float fraction = std::max(0.0f, h.hp / h.maxHp);
@@ -1271,41 +1295,62 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
else else
{ {
BuildingId id = buildingAtTile(tile); const QVector2D worldPos = widgetToWorld(event->pos());
if (id == kInvalidBuildingId) const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos);
if (hitEntity != entt::null)
{ {
id = siteAtTile(tile); m_selectedBuildingIds.clear();
}
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); emit selectionChanged(m_selectedBuildingIds);
m_selectedEntity = hitEntity;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(hitEntity));
} }
else 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); emit selectionChanged(m_selectedBuildingIds);
} }
m_boxSelecting = true; else
m_boxStartTile = tile; {
m_boxCurrentTile = tile; 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 "FireEvent.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
#include "EntitySelectedEvent.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "Rotation.h" #include "Rotation.h"
#include "Tick.h" #include "Tick.h"
@@ -106,6 +107,7 @@ private:
const BuildingDef* findBuildingDef(BuildingType type) const; const BuildingDef* findBuildingDef(BuildingType type) const;
BuildingId buildingAtTile(QPoint tile) const; BuildingId buildingAtTile(QPoint tile) const;
BuildingId siteAtTile(QPoint tile) const; BuildingId siteAtTile(QPoint tile) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint bodyTile, void drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color); Rotation direction, const QColor& color);
@@ -166,6 +168,7 @@ private:
bool m_debugDraw; bool m_debugDraw;
std::vector<BuildingId> m_selectedBuildingIds; std::vector<BuildingId> m_selectedBuildingIds;
std::optional<entt::entity> m_selectedEntity;
bool m_boxSelecting; bool m_boxSelecting;
QPoint m_boxStartTile; QPoint m_boxStartTile;
QPoint m_boxCurrentTile; QPoint m_boxCurrentTile;

View File

@@ -13,6 +13,16 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include "BeltSystem.h" #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 "TickAdvancedEvent.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
@@ -22,6 +32,7 @@
#include "Rotation.h" #include "Rotation.h"
#include "ShipLayoutPreview.h" #include "ShipLayoutPreview.h"
#include "Simulation.h" #include "Simulation.h"
#include "WeaponComponent.h"
namespace namespace
{ {
@@ -136,19 +147,39 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
connect(m_filterBList, &QListWidget::itemChanged, connect(m_filterBList, &QListWidget::itemChanged,
this, &SelectedBuildingPanel::onSplitterFilterChanged); 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(); buildEmpty();
registerForEvent(); registerForEvents();
} }
SelectedBuildingPanel::~SelectedBuildingPanel() SelectedBuildingPanel::~SelectedBuildingPanel()
{ {
unregisterForEvent(); unregisterForEvents();
} }
void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& ids) void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
{ {
m_selectedBuildingIds = ids; m_selectedBuildingIds = ids;
if (!ids.empty())
{
clearEntityDisplay();
}
rebuild(); rebuild();
} }
@@ -168,7 +199,7 @@ void SelectedBuildingPanel::rebuild()
} }
} }
void SelectedBuildingPanel::buildEmpty() void SelectedBuildingPanel::clearContent()
{ {
m_singleBuildingId = kInvalidBuildingId; m_singleBuildingId = kInvalidBuildingId;
m_titleLabel->hide(); m_titleLabel->hide();
@@ -183,6 +214,14 @@ void SelectedBuildingPanel::buildEmpty()
m_buffersLabel->hide(); m_buffersLabel->hide();
} }
void SelectedBuildingPanel::buildEmpty()
{
clearContent();
m_entityTitleLabel->hide();
m_entityStatsPanel->hide();
m_stationStatsLabel->hide();
}
void SelectedBuildingPanel::buildSingle(BuildingId id) void SelectedBuildingPanel::buildSingle(BuildingId id)
{ {
m_singleBuildingId = 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*/) void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
{ {
if (m_selectedEntity.has_value())
{
refreshEntityStats();
return;
}
if (m_singleBuildingId == kInvalidBuildingId) { return; } if (m_singleBuildingId == kInvalidBuildingId) { return; }
const Building* b = m_sim->buildings().findBuilding(m_singleBuildingId); const Building* b = m_sim->buildings().findBuilding(m_singleBuildingId);
if (b) 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) "))) if (m_titleLabel->text().startsWith(tr("(Building) ")))
{ {
rebuild(); rebuild();
@@ -688,3 +731,129 @@ void SelectedBuildingPanel::onClearBelt()
m_sim->belts().clearTiles(tiles); 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 #pragma once
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include <QPoint> #include <QPoint>
#include <QWidget> #include <QWidget>
#include "entt/entity/entity.hpp"
#include "Building.h" #include "Building.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "EntitySelectedEvent.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "RecipesConfig.h" #include "RecipesConfig.h"
@@ -18,6 +22,7 @@
class Simulation; class Simulation;
class ShipLayoutPreview; class ShipLayoutPreview;
class ShipStatsPanel;
class QLabel; class QLabel;
class QComboBox; class QComboBox;
class QListWidget; class QListWidget;
@@ -25,7 +30,7 @@ class QPushButton;
class QVBoxLayout; class QVBoxLayout;
class SelectedBuildingPanel : public QWidget, class SelectedBuildingPanel : public QWidget,
public EventHandler<TickAdvancedEvent> public CombinedEventHandler<TickAdvancedEvent, EntitySelectedEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -42,6 +47,7 @@ public slots:
private: private:
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override; void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
private slots: private slots:
void onRecipeChanged(int comboIndex); void onRecipeChanged(int comboIndex);
@@ -80,4 +86,14 @@ private:
BuildingId m_singleBuildingId; BuildingId m_singleBuildingId;
QPoint m_splitterTile; QPoint m_splitterTile;
std::string m_currentRecipeId; 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 std::vector<PlacedModule>& modules)
{ {
const ShipStats stats = calculateShipStats(*m_config, shipId, level, 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( void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f))); {
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( m_speedLabel->setText(
tr("Max Speed: %1 tiles/s").arg(fmt(stats.maxSpeed_tps))); tr("Max Speed: %1 tiles/s").arg(fmt(stats.maxSpeed_tps)));
m_sensorRangeLabel->setText( m_sensorRangeLabel->setText(

View File

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