add ship stats panel to ship layout dialog
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
218
src/lib/sim/ShipStatsCalculator.cpp
Normal file
218
src/lib/sim/ShipStatsCalculator.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#include "ShipStatsCalculator.h"
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
ShipStats calculateShipStats(const GameConfig& config,
|
||||
const std::string& shipId,
|
||||
int level,
|
||||
const std::vector<PlacedModule>& 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<double>(level);
|
||||
const double tileSize = config.world.tileSize_m;
|
||||
|
||||
// --- Base hull stats (convert from SI to display units) ------------------
|
||||
result.hp = static_cast<float>(
|
||||
shipDef->health.hpFormula.evaluate(x));
|
||||
result.maxSpeed_tps = static_cast<float>(
|
||||
shipDef->movement.speedFormula.evaluate(x) / tileSize);
|
||||
result.sensorRange_tiles = static_cast<float>(
|
||||
shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize);
|
||||
result.mainAcceleration_tpss = static_cast<float>(
|
||||
shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize);
|
||||
result.maneuveringAcceleration_tpss = static_cast<float>(
|
||||
shipDef->movement.maneuveringAccelerationFormula.evaluate(x) / tileSize);
|
||||
result.angularAcceleration_radpss = static_cast<float>(
|
||||
shipDef->movement.angularAccelerationFormula.evaluate(x));
|
||||
result.maxRotationSpeed_radps = static_cast<float>(
|
||||
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<WeaponInstance> weaponInstances;
|
||||
std::vector<SalvageInstance> salvageInstances;
|
||||
std::vector<RepairInstance> repairInstances;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
if (!def) { continue; }
|
||||
|
||||
const double mx = static_cast<double>(def->playerProductionLevel);
|
||||
|
||||
if (def->weaponCapability)
|
||||
{
|
||||
WeaponInstance wi;
|
||||
wi.damage = static_cast<float>(def->weaponCapability->damageFormula.evaluate(mx));
|
||||
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRangeFormula.evaluate(mx) / tileSize);
|
||||
wi.rate_hz = static_cast<float>(def->weaponCapability->attackRateFormula.evaluate(mx));
|
||||
weaponInstances.push_back(wi);
|
||||
}
|
||||
if (def->salvageCapability)
|
||||
{
|
||||
SalvageInstance si;
|
||||
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize);
|
||||
si.rate = static_cast<float>(def->salvageCapability->collectionRateFormula.evaluate(mx));
|
||||
salvageInstances.push_back(si);
|
||||
}
|
||||
if (def->repairCapability)
|
||||
{
|
||||
RepairInstance ri;
|
||||
ri.rate_hps = static_cast<float>(def->repairCapability->repairRateFormula.evaluate(mx));
|
||||
ri.range_tiles = static_cast<float>(def->repairCapability->repairRangeFormula.evaluate(mx) / tileSize);
|
||||
repairInstances.push_back(ri);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pass 2: accumulate passive stat modifiers ---------------------------
|
||||
// Mirrors ShipSystem::spawn() routing logic exactly.
|
||||
std::map<std::string, std::pair<double, double>> hullMods;
|
||||
std::map<std::string, std::pair<double, double>> weaponMods;
|
||||
std::map<std::string, std::pair<double, double>> salvageMods;
|
||||
std::map<std::string, std::pair<double, double>> repairMods;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
if (!def) { continue; }
|
||||
|
||||
const double mx = static_cast<double>(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<std::string, std::pair<double, double>>* target = &hullMods;
|
||||
if (isWeaponStat) { target = &weaponMods; }
|
||||
if (isSalvageStat) { target = &salvageMods; }
|
||||
if (isRepairStat) { target = &repairMods; }
|
||||
|
||||
std::pair<double, double>& 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<std::string, std::pair<double, double>>* allModMaps[] = {
|
||||
&hullMods, &weaponMods, &salvageMods, &repairMods
|
||||
};
|
||||
for (const char* stat : kRangeStats)
|
||||
{
|
||||
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
||||
{
|
||||
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
||||
if (it != mods->end())
|
||||
{
|
||||
it->second.second /= tileSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto applyMod = [](float& stat, const std::string& name,
|
||||
const std::map<std::string, std::pair<double, double>>& mods)
|
||||
{
|
||||
const std::map<std::string, std::pair<double, double>>::const_iterator it = mods.find(name);
|
||||
if (it != mods.end())
|
||||
{
|
||||
stat = static_cast<float>(
|
||||
static_cast<double>(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;
|
||||
}
|
||||
49
src/lib/sim/ShipStatsCalculator.h
Normal file
49
src/lib/sim/ShipStatsCalculator.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<WeaponStats> weapons;
|
||||
std::optional<SalvageStats> salvage;
|
||||
std::optional<RepairStats> repair;
|
||||
};
|
||||
|
||||
ShipStats calculateShipStats(const GameConfig& config,
|
||||
const std::string& shipId,
|
||||
int level,
|
||||
const std::vector<PlacedModule>& modules);
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ShipLayoutDialog.h"
|
||||
#include "ShipStatsPanel.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <functional>
|
||||
@@ -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<PlacedModule>& 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<PlacedModule>& modu
|
||||
|
||||
rebuildOccupancy();
|
||||
updateGridWidget();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
@@ -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<std::string> rotatedMask(const ModuleDef& def, Rotation rotation) const;
|
||||
void loadLayoutBlueprint(const std::vector<PlacedModule>& modules);
|
||||
@@ -68,6 +70,7 @@ private:
|
||||
std::vector<QPushButton*> m_moduleButtons;
|
||||
QPushButton* m_removeButton;
|
||||
QWidget* m_gridWidget;
|
||||
ShipStatsPanel* m_statsPanel;
|
||||
|
||||
std::optional<ShipLayoutConfig> m_result;
|
||||
};
|
||||
|
||||
168
src/ui/ShipStatsPanel.cpp
Normal file
168
src/ui/ShipStatsPanel.cpp
Normal file
@@ -0,0 +1,168 @@
|
||||
#include "ShipStatsPanel.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "GameConfig.h"
|
||||
#include "ShipStatsCalculator.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
QString fmt(float value)
|
||||
{
|
||||
return QString::number(static_cast<double>(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<PlacedModule>& modules)
|
||||
{
|
||||
const ShipStats stats = calculateShipStats(*m_config, shipId, level, modules);
|
||||
|
||||
m_hpLabel->setText(
|
||||
tr("HP: %1").arg(static_cast<int>(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);
|
||||
}
|
||||
}
|
||||
46
src/ui/ShipStatsPanel.h
Normal file
46
src/ui/ShipStatsPanel.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#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<PlacedModule>& 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;
|
||||
};
|
||||
Reference in New Issue
Block a user