diff --git a/docs/requirements.md b/docs/requirements.md index 4e582f8..44c90ba 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -222,11 +222,30 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened. The dialog contains: - - **Left**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode. - - **Center**: A grid of module selection buttons (one per module type defined in `modules.toml`) plus a "Remove" button. Each module button shows the module id and its glyph. - - **Right**: The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). + - **Top**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode. + - **Left** (below the grid): The ship stats panel (see REQ-MOD-UI-STATS-PANEL). + - **Center** (below the grid): A grid of module selection buttons (one per module type defined in `modules.toml`) plus a "Remove" button. Each module button shows the module id and its glyph. + - **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). - **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed. +- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed at the schematic's `player_production_level`, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid. + + The panel always shows all hull stats as final computed values: + - HP + - Max linear speed + - Sensor range + - Main acceleration + - Maneuvering acceleration + - Angular acceleration + - Max rotation speed + + In addition, the panel shows capability module stats conditioned on which capability module types are present in the current layout: + - **Weapons** (shown only if at least one weapon module is placed): combined DPS = Σ(damage_i × attack_rate_i) across all weapon module instances; maximum range = max(attack_range_i) across all weapon module instances. + - **Salvage** (shown only if at least one salvage module is placed): combined collection rate = Σ(collection_rate_i) across all salvage module instances; maximum range = max(collection_range_i) across all salvage module instances. + - **Repair** (shown only if at least one repair module is placed): combined repair rate = Σ(repair_rate_i) across all repair module instances; maximum range = max(repair_range_i) across all repair module instances. + + All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. Each capability module instance uses its own `player_production_level` for formula evaluation. + - REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog). ### Layout Blueprints diff --git a/src/lib/sim/CMakeLists.txt b/src/lib/sim/CMakeLists.txt index 0f910e8..db919bf 100644 --- a/src/lib/sim/CMakeLists.txt +++ b/src/lib/sim/CMakeLists.txt @@ -7,6 +7,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h + ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h PARENT_SCOPE ) @@ -17,6 +18,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}/ShipStatsCalculator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp PARENT_SCOPE ) diff --git a/src/lib/sim/ShipStatsCalculator.cpp b/src/lib/sim/ShipStatsCalculator.cpp new file mode 100644 index 0000000..bac58d8 --- /dev/null +++ b/src/lib/sim/ShipStatsCalculator.cpp @@ -0,0 +1,218 @@ +#include "ShipStatsCalculator.h" + +#include +#include + +ShipStats calculateShipStats(const GameConfig& config, + const std::string& shipId, + int level, + const std::vector& modules) +{ + ShipStats result{}; + + const ShipDef* shipDef = nullptr; + for (const ShipDef& d : config.ships.ships) + { + if (d.id == shipId) { shipDef = &d; break; } + } + if (!shipDef) { return result; } + + auto findModuleDef = [&](const std::string& id) -> const ModuleDef* + { + for (const ModuleDef& d : config.modules.modules) + { + if (d.id == id) { return &d; } + } + return nullptr; + }; + + const double x = static_cast(level); + const double tileSize = config.world.tileSize_m; + + // --- Base hull stats (convert from SI to display units) ------------------ + result.hp = static_cast( + shipDef->health.hpFormula.evaluate(x)); + result.maxSpeed_tps = static_cast( + shipDef->movement.speedFormula.evaluate(x) / tileSize); + result.sensorRange_tiles = static_cast( + shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize); + result.mainAcceleration_tpss = static_cast( + shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize); + result.maneuveringAcceleration_tpss = static_cast( + shipDef->movement.maneuveringAccelerationFormula.evaluate(x) / tileSize); + result.angularAcceleration_radpss = static_cast( + shipDef->movement.angularAccelerationFormula.evaluate(x)); + result.maxRotationSpeed_radps = static_cast( + shipDef->movement.maxRotationSpeedFormula.evaluate(x)); + + // --- Pass 1: base capability stats per module instance ------------------- + struct WeaponInstance { float damage; float range_tiles; float rate_hz; }; + struct SalvageInstance { float range_tiles; float rate; }; + struct RepairInstance { float rate_hps; float range_tiles; }; + + std::vector weaponInstances; + std::vector salvageInstances; + std::vector repairInstances; + + for (const PlacedModule& pm : modules) + { + const ModuleDef* def = findModuleDef(pm.moduleId); + if (!def) { continue; } + + const double mx = static_cast(def->playerProductionLevel); + + if (def->weaponCapability) + { + WeaponInstance wi; + wi.damage = static_cast(def->weaponCapability->damageFormula.evaluate(mx)); + wi.range_tiles = static_cast(def->weaponCapability->attackRangeFormula.evaluate(mx) / tileSize); + wi.rate_hz = static_cast(def->weaponCapability->attackRateFormula.evaluate(mx)); + weaponInstances.push_back(wi); + } + if (def->salvageCapability) + { + SalvageInstance si; + si.range_tiles = static_cast(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize); + si.rate = static_cast(def->salvageCapability->collectionRateFormula.evaluate(mx)); + salvageInstances.push_back(si); + } + if (def->repairCapability) + { + RepairInstance ri; + ri.rate_hps = static_cast(def->repairCapability->repairRateFormula.evaluate(mx)); + ri.range_tiles = static_cast(def->repairCapability->repairRangeFormula.evaluate(mx) / tileSize); + repairInstances.push_back(ri); + } + } + + // --- Pass 2: accumulate passive stat modifiers --------------------------- + // Mirrors ShipSystem::spawn() routing logic exactly. + std::map> hullMods; + std::map> weaponMods; + std::map> salvageMods; + std::map> repairMods; + + for (const PlacedModule& pm : modules) + { + const ModuleDef* def = findModuleDef(pm.moduleId); + if (!def) { continue; } + + const double mx = static_cast(def->playerProductionLevel); + + for (const ModuleStatModifier& sm : def->statModifiers) + { + const double val = sm.formula.evaluate(mx); + + const bool isWeaponStat = (sm.stat == "damage" + || sm.stat == "attack_range" + || sm.stat == "attack_rate"); + const bool isSalvageStat = (sm.stat == "collection_range" + || sm.stat == "cargo_capacity"); + const bool isRepairStat = (sm.stat == "repair_rate" + || sm.stat == "repair_range"); + + std::map>* target = &hullMods; + if (isWeaponStat) { target = &weaponMods; } + if (isSalvageStat) { target = &salvageMods; } + if (isRepairStat) { target = &repairMods; } + + std::pair& acc = (*target)[sm.stat]; + if (sm.modifierType == "multiplicative") + { + acc.first += (val - 1.0); + } + else + { + acc.second += val; + } + } + } + + // Range additive modifiers are in metres in config; convert to tiles. + const char* const kRangeStats[] = { + "sensor_range", "attack_range", "collection_range", "repair_range" + }; + std::map>* allModMaps[] = { + &hullMods, &weaponMods, &salvageMods, &repairMods + }; + for (const char* stat : kRangeStats) + { + for (std::map>* mods : allModMaps) + { + std::map>::iterator it = mods->find(stat); + if (it != mods->end()) + { + it->second.second /= tileSize; + } + } + } + + auto applyMod = [](float& stat, const std::string& name, + const std::map>& mods) + { + const std::map>::const_iterator it = mods.find(name); + if (it != mods.end()) + { + stat = static_cast( + static_cast(stat) * (1.0 + it->second.first) + + it->second.second); + } + }; + + // Apply hull modifiers. + applyMod(result.hp, "hp", hullMods); + applyMod(result.maxSpeed_tps, "speed", hullMods); + applyMod(result.mainAcceleration_tpss, "main_acceleration", hullMods); + applyMod(result.maneuveringAcceleration_tpss, "maneuvering_acceleration", hullMods); + applyMod(result.angularAcceleration_radpss, "angular_acceleration", hullMods); + applyMod(result.maxRotationSpeed_radps, "max_rotation_speed", hullMods); + applyMod(result.sensorRange_tiles, "sensor_range", hullMods); + + // Apply weapon modifiers and compute combined stats. + if (!weaponInstances.empty()) + { + float combinedDps = 0.0f; + float maxRange = 0.0f; + for (WeaponInstance& wi : weaponInstances) + { + applyMod(wi.damage, "damage", weaponMods); + applyMod(wi.range_tiles, "attack_range", weaponMods); + applyMod(wi.rate_hz, "attack_rate", weaponMods); + combinedDps += wi.damage * wi.rate_hz; + if (wi.range_tiles > maxRange) { maxRange = wi.range_tiles; } + } + result.weapons = ShipStats::WeaponStats{combinedDps, maxRange}; + } + + // Apply salvage modifiers and compute combined stats. + if (!salvageInstances.empty()) + { + float combinedRate = 0.0f; + float maxRange = 0.0f; + for (SalvageInstance& si : salvageInstances) + { + applyMod(si.range_tiles, "collection_range", salvageMods); + applyMod(si.rate, "collection_rate", salvageMods); + combinedRate += si.rate; + if (si.range_tiles > maxRange) { maxRange = si.range_tiles; } + } + result.salvage = ShipStats::SalvageStats{combinedRate, maxRange}; + } + + // Apply repair modifiers and compute combined stats. + if (!repairInstances.empty()) + { + float combinedRate = 0.0f; + float maxRange = 0.0f; + for (RepairInstance& ri : repairInstances) + { + applyMod(ri.rate_hps, "repair_rate", repairMods); + applyMod(ri.range_tiles, "repair_range", repairMods); + combinedRate += ri.rate_hps; + if (ri.range_tiles > maxRange) { maxRange = ri.range_tiles; } + } + result.repair = ShipStats::RepairStats{combinedRate, maxRange}; + } + + return result; +} diff --git a/src/lib/sim/ShipStatsCalculator.h b/src/lib/sim/ShipStatsCalculator.h new file mode 100644 index 0000000..41f84d5 --- /dev/null +++ b/src/lib/sim/ShipStatsCalculator.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "GameConfig.h" +#include "ShipLayout.h" + +// 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². +struct ShipStats +{ + float hp; + float maxSpeed_tps; + float sensorRange_tiles; + float mainAcceleration_tpss; + float maneuveringAcceleration_tpss; + float angularAcceleration_radpss; + float maxRotationSpeed_radps; + + struct WeaponStats + { + float combinedDps; + float maxRange_tiles; + }; + + struct SalvageStats + { + float combinedCollectionRate; + float maxRange_tiles; + }; + + struct RepairStats + { + float combinedRepairRate_hps; + float maxRange_tiles; + }; + + std::optional weapons; + std::optional salvage; + std::optional repair; +}; + +ShipStats calculateShipStats(const GameConfig& config, + const std::string& shipId, + int level, + const std::vector& modules); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 3cc2d1d..e9730d3 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -10,6 +10,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h + ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h PARENT_SCOPE ) @@ -24,5 +25,6 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp PARENT_SCOPE ) diff --git a/src/ui/ShipLayoutDialog.cpp b/src/ui/ShipLayoutDialog.cpp index 9376055..c0f18d4 100644 --- a/src/ui/ShipLayoutDialog.cpp +++ b/src/ui/ShipLayoutDialog.cpp @@ -1,4 +1,5 @@ #include "ShipLayoutDialog.h" +#include "ShipStatsPanel.h" #include #include @@ -397,6 +398,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, , m_currentRotation(Rotation::East) , m_removeButton(nullptr) , m_gridWidget(nullptr) + , m_statsPanel(nullptr) { setWindowTitle(tr("Configure Ship Layout")); setModal(true); @@ -437,17 +439,24 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, rebuildOccupancy(); // --- UI layout --- - QHBoxLayout* mainLayout = new QHBoxLayout(this); + QVBoxLayout* outerLayout = new QVBoxLayout(this); - // Left: grid widget. + // Top: grid widget. LayoutGridWidget* gridW = new LayoutGridWidget(this, this); gridW->setGridData(&m_grid, m_rows, m_cols, &m_placedModules, m_config); gridW->setGhostData(m_activeModuleIndex, m_currentRotation); m_gridWidget = gridW; - mainLayout->addWidget(m_gridWidget); + outerLayout->addWidget(m_gridWidget, 0, Qt::AlignHCenter | Qt::AlignTop); - // Right: module buttons + confirm/cancel. - QVBoxLayout* rightLayout = new QVBoxLayout(); + // Middle: three-column area (stats | module buttons | blueprints). + QHBoxLayout* columnsLayout = new QHBoxLayout(); + + // Left column: ship stats panel. + m_statsPanel = new ShipStatsPanel(config, this); + columnsLayout->addWidget(m_statsPanel); + + // Center column: module selection buttons. + QVBoxLayout* centerLayout = new QVBoxLayout(); QGridLayout* buttonGrid = new QGridLayout(); buttonGrid->setSpacing(4); @@ -508,30 +517,35 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, updateGridWidget(); }); - rightLayout->addLayout(buttonGrid); - rightLayout->addStretch(); + centerLayout->addLayout(buttonGrid); + centerLayout->addStretch(); - // Confirm / Cancel buttons. - QHBoxLayout* bottomBar = new QHBoxLayout(); - QPushButton* confirmBtn = new QPushButton(tr("Confirm"), this); - QPushButton* cancelBtn = new QPushButton(tr("Cancel"), this); - bottomBar->addWidget(confirmBtn); - bottomBar->addWidget(cancelBtn); - rightLayout->addLayout(bottomBar); + columnsLayout->addLayout(centerLayout); - connect(confirmBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onConfirm); - connect(cancelBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onCancel); - - mainLayout->addLayout(rightLayout); - - // Right: blueprint panel (third column). + // Right column: blueprint panel. ShipLayoutBlueprintPanel* bpPanel = new ShipLayoutBlueprintPanel( allBlueprints, m_shipId, [this]() { return m_placedModules; }, [this](const std::vector& mods) { loadLayoutBlueprint(mods); }, this); - mainLayout->addWidget(bpPanel); + columnsLayout->addWidget(bpPanel); + + outerLayout->addLayout(columnsLayout, 1); + + // Bottom: confirm / cancel buttons. + QHBoxLayout* bottomBar = new QHBoxLayout(); + QPushButton* confirmBtn = new QPushButton(tr("Confirm"), this); + QPushButton* cancelBtn = new QPushButton(tr("Cancel"), this); + bottomBar->addWidget(confirmBtn); + bottomBar->addWidget(cancelBtn); + outerLayout->addLayout(bottomBar); + + connect(confirmBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onConfirm); + connect(cancelBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onCancel); + + // Initial stats display. + updateStats(); // Grid click handler. connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) { @@ -551,6 +565,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, m_placedModules.erase(m_placedModules.begin() + idx); rebuildOccupancy(); updateGridWidget(); + updateStats(); } } return; @@ -567,6 +582,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, m_placedModules.push_back(pm); rebuildOccupancy(); updateGridWidget(); + updateStats(); } }); } @@ -690,6 +706,20 @@ void ShipLayoutDialog::updateGridWidget() gridW->update(); } +void ShipLayoutDialog::updateStats() +{ + int level = 1; + for (const ShipDef& def : m_config->ships.ships) + { + if (def.id == m_shipId) + { + level = def.schematic.playerProductionLevel; + break; + } + } + m_statsPanel->refresh(m_shipId, level, m_placedModules); +} + bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const { @@ -795,4 +825,5 @@ void ShipLayoutDialog::loadLayoutBlueprint(const std::vector& modu rebuildOccupancy(); updateGridWidget(); + updateStats(); } diff --git a/src/ui/ShipLayoutDialog.h b/src/ui/ShipLayoutDialog.h index e73dc3a..25475eb 100644 --- a/src/ui/ShipLayoutDialog.h +++ b/src/ui/ShipLayoutDialog.h @@ -13,6 +13,7 @@ #include "ShipLayoutBlueprint.h" class QPushButton; +class ShipStatsPanel; class ShipLayoutDialog : public QDialog { @@ -49,6 +50,7 @@ private: void rebuildOccupancy(); void updateGridWidget(); + void updateStats(); bool canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const; std::vector rotatedMask(const ModuleDef& def, Rotation rotation) const; void loadLayoutBlueprint(const std::vector& modules); @@ -68,6 +70,7 @@ private: std::vector m_moduleButtons; QPushButton* m_removeButton; QWidget* m_gridWidget; + ShipStatsPanel* m_statsPanel; std::optional m_result; }; diff --git a/src/ui/ShipStatsPanel.cpp b/src/ui/ShipStatsPanel.cpp new file mode 100644 index 0000000..7837504 --- /dev/null +++ b/src/ui/ShipStatsPanel.cpp @@ -0,0 +1,168 @@ +#include "ShipStatsPanel.h" + +#include +#include +#include + +#include "GameConfig.h" +#include "ShipStatsCalculator.h" + +namespace +{ + +QString fmt(float value) +{ + return QString::number(static_cast(value), 'f', 1); +} + +QLabel* makeSectionHeader(const QString& text, QWidget* parent) +{ + QLabel* label = new QLabel(text, parent); + QFont f = label->font(); + f.setBold(true); + label->setFont(f); + return label; +} + +QLabel* makeStatLabel(QWidget* parent) +{ + return new QLabel(parent); +} + +} // namespace + + +ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent) + : QWidget(parent) + , m_config(config) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(4, 4, 4, 4); + layout->setSpacing(2); + layout->setAlignment(Qt::AlignTop); + + // Hull stats — always visible. + m_hpLabel = makeStatLabel(this); + m_speedLabel = makeStatLabel(this); + m_sensorRangeLabel = makeStatLabel(this); + m_mainAccelLabel = makeStatLabel(this); + m_maneuveringAccelLabel = makeStatLabel(this); + m_angularAccelLabel = makeStatLabel(this); + m_maxRotSpeedLabel = makeStatLabel(this); + + layout->addWidget(m_hpLabel); + layout->addWidget(m_speedLabel); + layout->addWidget(m_sensorRangeLabel); + layout->addWidget(m_mainAccelLabel); + layout->addWidget(m_maneuveringAccelLabel); + layout->addWidget(m_angularAccelLabel); + layout->addWidget(m_maxRotSpeedLabel); + + // Weapon capability section. + m_weaponSection = new QWidget(this); + { + QVBoxLayout* sl = new QVBoxLayout(m_weaponSection); + sl->setContentsMargins(0, 4, 0, 0); + sl->setSpacing(2); + sl->addWidget(makeSectionHeader(tr("Weapons"), m_weaponSection)); + m_weaponDpsLabel = makeStatLabel(m_weaponSection); + m_weaponRangeLabel = makeStatLabel(m_weaponSection); + sl->addWidget(m_weaponDpsLabel); + sl->addWidget(m_weaponRangeLabel); + } + m_weaponSection->setVisible(false); + layout->addWidget(m_weaponSection); + + // Salvage capability section. + m_salvageSection = new QWidget(this); + { + QVBoxLayout* sl = new QVBoxLayout(m_salvageSection); + sl->setContentsMargins(0, 4, 0, 0); + sl->setSpacing(2); + sl->addWidget(makeSectionHeader(tr("Salvage"), m_salvageSection)); + m_salvageRateLabel = makeStatLabel(m_salvageSection); + m_salvageRangeLabel = makeStatLabel(m_salvageSection); + sl->addWidget(m_salvageRateLabel); + sl->addWidget(m_salvageRangeLabel); + } + m_salvageSection->setVisible(false); + layout->addWidget(m_salvageSection); + + // Repair capability section. + m_repairSection = new QWidget(this); + { + QVBoxLayout* sl = new QVBoxLayout(m_repairSection); + sl->setContentsMargins(0, 4, 0, 0); + sl->setSpacing(2); + sl->addWidget(makeSectionHeader(tr("Repair"), m_repairSection)); + m_repairRateLabel = makeStatLabel(m_repairSection); + m_repairRangeLabel = makeStatLabel(m_repairSection); + sl->addWidget(m_repairRateLabel); + sl->addWidget(m_repairRangeLabel); + } + m_repairSection->setVisible(false); + layout->addWidget(m_repairSection); + + layout->addStretch(); +} + +void ShipStatsPanel::refresh(const std::string& shipId, + int level, + const std::vector& modules) +{ + const ShipStats stats = calculateShipStats(*m_config, shipId, level, modules); + + m_hpLabel->setText( + tr("HP: %1").arg(static_cast(stats.hp + 0.5f))); + m_speedLabel->setText( + tr("Max Speed: %1 tiles/s").arg(fmt(stats.maxSpeed_tps))); + m_sensorRangeLabel->setText( + tr("Sensor Range: %1 tiles").arg(fmt(stats.sensorRange_tiles))); + m_mainAccelLabel->setText( + tr("Main Accel: %1 tiles/s\xc2\xb2").arg(fmt(stats.mainAcceleration_tpss))); + m_maneuveringAccelLabel->setText( + tr("Maneuvering Accel: %1 tiles/s\xc2\xb2").arg(fmt(stats.maneuveringAcceleration_tpss))); + m_angularAccelLabel->setText( + tr("Angular Accel: %1 rad/s\xc2\xb2").arg(fmt(stats.angularAcceleration_radpss))); + m_maxRotSpeedLabel->setText( + tr("Max Rotation: %1 rad/s").arg(fmt(stats.maxRotationSpeed_radps))); + + if (stats.weapons.has_value()) + { + m_weaponDpsLabel->setText( + tr("DPS: %1").arg(fmt(stats.weapons->combinedDps))); + m_weaponRangeLabel->setText( + tr("Range: %1 tiles").arg(fmt(stats.weapons->maxRange_tiles))); + m_weaponSection->setVisible(true); + } + else + { + m_weaponSection->setVisible(false); + } + + if (stats.salvage.has_value()) + { + m_salvageRateLabel->setText( + tr("Collection Rate: %1/s").arg(fmt(stats.salvage->combinedCollectionRate))); + m_salvageRangeLabel->setText( + tr("Range: %1 tiles").arg(fmt(stats.salvage->maxRange_tiles))); + m_salvageSection->setVisible(true); + } + else + { + m_salvageSection->setVisible(false); + } + + if (stats.repair.has_value()) + { + m_repairRateLabel->setText( + tr("Repair Rate: %1 HP/s").arg(fmt(stats.repair->combinedRepairRate_hps))); + m_repairRangeLabel->setText( + tr("Range: %1 tiles").arg(fmt(stats.repair->maxRange_tiles))); + m_repairSection->setVisible(true); + } + else + { + m_repairSection->setVisible(false); + } +} diff --git a/src/ui/ShipStatsPanel.h b/src/ui/ShipStatsPanel.h new file mode 100644 index 0000000..18ae694 --- /dev/null +++ b/src/ui/ShipStatsPanel.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#include + +#include "ShipLayout.h" + +struct GameConfig; +class QLabel; + +class ShipStatsPanel : public QWidget +{ + Q_OBJECT + +public: + explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr); + + void refresh(const std::string& shipId, + int level, + const std::vector& modules); + +private: + const GameConfig* m_config; + + QLabel* m_hpLabel; + QLabel* m_speedLabel; + QLabel* m_sensorRangeLabel; + QLabel* m_mainAccelLabel; + QLabel* m_maneuveringAccelLabel; + QLabel* m_angularAccelLabel; + QLabel* m_maxRotSpeedLabel; + + QWidget* m_weaponSection; + QLabel* m_weaponDpsLabel; + QLabel* m_weaponRangeLabel; + + QWidget* m_salvageSection; + QLabel* m_salvageRateLabel; + QLabel* m_salvageRangeLabel; + + QWidget* m_repairSection; + QLabel* m_repairRateLabel; + QLabel* m_repairRangeLabel; +};