Compare commits
2 Commits
37a70ea321
...
c64d31fa46
| Author | SHA1 | Date | |
|---|---|---|---|
| c64d31fa46 | |||
| f097e9a25f |
@@ -274,13 +274,13 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
## Threat Level & Enemy Waves
|
||||
|
||||
- REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas.
|
||||
- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over real game time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER), clamped to a minimum of 0 (negative formula values are treated as 0). The rate is constant within each boss wave cycle and steps up each time `x` increments. Threat accumulation continues uninterrupted during quiet windows (REQ-WAV-QUIET). Example: `1*x - 30` yields 0 threat/s when x ≤ 30 and increases linearly beyond that.
|
||||
- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over real game time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER), clamped to a minimum of 0 (negative formula values are treated as 0). The rate is constant within each boss wave cycle and steps up each time `x` increments. Threat accumulation is paused during quiet windows (REQ-WAV-QUIET). Example: `1*x - 30` yields 0 threat/s when x ≤ 30 and increases linearly beyond that.
|
||||
- REQ-WAV-GAP: At game start and immediately after each normal wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`]. The gap timer does not advance while inside a quiet window (REQ-WAV-QUIET); if a gap would expire inside a quiet window, its expiry is deferred until the quiet window ends.
|
||||
- REQ-WAV-TRIGGER: When the gap timer expires outside a quiet window, a normal wave is triggered. Ships are selected one at a time: from all schematics whose `threat.cost_formula` evaluates to > 0 at the current enemy ship level, uniformly randomly pick one whose cost fits the remaining threat budget. Repeat until no eligible schematic fits. Any remaining threat carries over to the next normal wave. A longer gap results in a larger wave. Because enemy ship level increases with the boss wave counter (REQ-WAV-SHIP-LEVEL), threat cost per ship rises as the game progresses.
|
||||
- REQ-WAV-SHIP-LEVEL: Each wave's (normal and boss) enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER). Per-ship stats and threat cost are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS).
|
||||
- REQ-WAV-BOSS-COUNTDOWN: A **boss countdown** timer starts at `world.toml [waves].boss_countdown_seconds` (default 300) at game start and counts down continuously in real game-time seconds. It is not paused during quiet windows. When it reaches 0, a boss wave is triggered (REQ-WAV-BOSS-TRIGGER). Immediately after the boss wave fires, `x` increments (REQ-WAV-BOSS-COUNTER) and a fresh countdown starts at the same configured value.
|
||||
- REQ-WAV-BOSS-ADVANCE: When the player destroys a set of enemy defence stations, the boss countdown is reduced by `world.toml [push].boss_advance_seconds` (default 60), clamped to a minimum of 0. Threat that would have accumulated during the skipped time is not added. If the countdown reaches 0 by this reduction, the boss wave is triggered immediately.
|
||||
- REQ-WAV-QUIET: A **quiet window** suppresses normal wave spawning around each boss wave. The pre-boss quiet window begins when the boss countdown falls to or below `world.toml [waves].boss_quiet_before_seconds` and ends when the countdown reaches 0. The post-boss quiet window begins immediately when the boss wave fires and lasts `world.toml [waves].boss_quiet_after_seconds` seconds. Threat continues to accumulate during both windows. The normal wave gap timer does not advance during either window (REQ-WAV-GAP). The new boss countdown runs during the post-boss quiet window.
|
||||
- REQ-WAV-QUIET: A **quiet window** suppresses normal wave spawning around each boss wave. The pre-boss quiet window begins when the boss countdown falls to or below `world.toml [waves].boss_quiet_before_seconds` and ends when the countdown reaches 0. The post-boss quiet window begins immediately when the boss wave fires and lasts `world.toml [waves].boss_quiet_after_seconds` seconds. Threat accumulation is paused during both windows. The normal wave gap timer does not advance during either window (REQ-WAV-GAP). The new boss countdown runs during the post-boss quiet window.
|
||||
- REQ-WAV-BOSS-TRIGGER: When the boss countdown reaches 0, a boss wave is triggered. Its threat budget is the sum of: (a) `world.toml [waves].boss_threat_duration_seconds` (default 60) multiplied by the current threat rate, and (b) all unspent threat carried over from normal waves. Ships are selected using the same random process as normal waves (REQ-WAV-TRIGGER). Any threat remaining unspent after ship selection carries over to the first normal wave of the new cycle.
|
||||
- REQ-WAV-DEFAULT-MODULES: Enemy ships spawned by waves use the `default_modules` list defined per schematic in `ships.toml`. The `default_modules` array uses the same format as layout blueprints (see Layout Blueprint TOML Format). If `default_modules` is absent or empty, the ship spawns with no modules. Invalid module instances (unknown type, position outside the grid, position on a non-buildable cell, or overlapping another module) are silently skipped.
|
||||
- REQ-WAV-SPAWN-DURATION: Ships in a wave are spawned one at a time over `world.toml [waves].spawn_duration_seconds`.
|
||||
@@ -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,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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
|
||||
void WaveSystem::tickThreatAccumulation()
|
||||
{
|
||||
TRACE();
|
||||
if (isInQuietWindow())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const double x = static_cast<double>(m_bossWaveCounter);
|
||||
const double rate = m_config.world.waves.threatRateFormula.evaluate(x);
|
||||
if (rate > 0.0)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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