add live ship stats panel
This commit is contained in:
@@ -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-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-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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +48,10 @@ private slots:
|
||||
|
||||
private:
|
||||
void updateInfoPanel(const ArenaStatus& status);
|
||||
void refreshEntityStats();
|
||||
|
||||
ArenaSimulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
ArenaView* m_arenaView;
|
||||
|
||||
std::vector<QPushButton*> m_speedButtons;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
21
src/lib/eventsystem/event/EntitySelectedEvent.h
Normal file
21
src/lib/eventsystem/event/EntitySelectedEvent.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
56
src/lib/sim/EntityHitTest.cpp
Normal file
56
src/lib/sim/EntityHitTest.cpp
Normal 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;
|
||||
}
|
||||
9
src/lib/sim/EntityHitTest.h
Normal file
9
src/lib/sim/EntityHitTest.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,6 +1295,26 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
}
|
||||
else
|
||||
{
|
||||
const QVector2D worldPos = widgetToWorld(event->pos());
|
||||
const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos);
|
||||
|
||||
if (hitEntity != entt::null)
|
||||
{
|
||||
m_selectedBuildingIds.clear();
|
||||
emit selectionChanged(m_selectedBuildingIds);
|
||||
m_selectedEntity = hitEntity;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectedEvent>(hitEntity));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_selectedEntity.has_value())
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectedEvent>(std::nullopt));
|
||||
}
|
||||
|
||||
BuildingId id = buildingAtTile(tile);
|
||||
if (id == kInvalidBuildingId)
|
||||
{
|
||||
@@ -1309,6 +1353,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user