give the selection cards their own parts instead of label blobs
The cards were assembled from plain labels carrying whole blocks of text. Replace those with the widget vocabulary the requirements describe, so a part means the same thing wherever it appears and a card is a list of parts rather than a string builder. The parts, all free of Simulation and GameConfig -- they take prepared values, and the contents work out what those are: - StatRow, BarRow, SectionBox: label/value line, captioned fill bar, captioned group. The bar is one part for three things: construction progress, production progress, and HP. - ItemChip / ItemChipRow: buffered items as icon, count and sub-line (REQ-UI-SINGLE-SELECTION). An input chip carries its per-cycle amount, an output chip its count against the buffer capacity. The chips are rebuilt only when the set of items changes, so a 30 Hz refresh moves numbers rather than widgets. - RecipeSummaryRow: inputs, arrow, outputs, cycle time (REQ-UI-RECIPE-SUMMARY), which is now the panel's only display of the cycle time. - CountRow, StatusPill, EmptyNote. Behaviour that changed with them: - The station card shows damage, range and fire rate as the requirement asks (REQ-UI-STATION-STATS-PANEL) rather than the combined DPS it showed before. - A ship's behaviour moves from a stats row into the card header (REQ-UI-SHIP-BEHAVIOR). ShipStatsPanel keeps setBehavior for the balancing tool's inspect window, which has no header to put it in. - A construction site's card shows a progress bar and the "no buffers until built" note (REQ-UI-SELECTION-CARD), and now also its recipe summary, since that is configuration and a site carries it (REQ-BLD-SITE-CONFIG). Costing a shipyard site's schematic needed computeShipyardRequiredMaterials to take a stored configuration as well as a live building -- one overload, so the module sum still exists once. ShipStatsPanel is rebuilt on StatRow, BarRow and SectionBox, so the selection card, the layout dialog's design preview and the balancing tool read alike. Those three parts are compiled into the balancing target, which does not link the ui library; keeping them sim-free is what makes that possible, and the build enforces it. Build clean, 541 tests pass, app and balancing tool both run with no Qt warnings. Visual check pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -10,7 +10,12 @@ set(TARGET_LIB_INCLUDE_DIRS
|
|||||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/external"
|
"${CMAKE_CURRENT_SOURCE_DIR}/external"
|
||||||
)
|
)
|
||||||
set(TARGET_UI_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/ui")
|
set(TARGET_UI_INCLUDE_DIRS
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/ui"
|
||||||
|
# The balancing target compiles a few ui files into itself rather than linking the
|
||||||
|
# ui library, and the ship stats panel is built from the selection card's parts.
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/ui/selection"
|
||||||
|
)
|
||||||
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
||||||
set(TARGET_BALANCING_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/balancing")
|
set(TARGET_BALANCING_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/balancing")
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.h
|
||||||
|
# The card parts the ship stats panel is built from. They are deliberately free of
|
||||||
|
# Simulation and GameConfig, which is what lets them come along here.
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/StatRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/BarRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SectionBox.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SelectionNames.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
|
||||||
# Shared world-space shapes so the arena keeps looking like the game
|
# Shared world-space shapes so the arena keeps looking like the game
|
||||||
@@ -26,6 +32,10 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/StatRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/BarRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SectionBox.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SelectionNames.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/WorldPrimitives.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/../ui/WorldPrimitives.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
|
|||||||
@@ -45,9 +45,17 @@ bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
|
|||||||
}
|
}
|
||||||
std::map<std::string, int>
|
std::map<std::string, int>
|
||||||
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
return computeShipyardRequiredMaterials(config, b.recipeId, b.shipLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, int>
|
||||||
|
computeShipyardRequiredMaterials(const GameConfig& config,
|
||||||
|
const std::string& recipeId,
|
||||||
|
const std::optional<ShipLayoutConfig>& shipLayout)
|
||||||
{
|
{
|
||||||
std::map<std::string, int> requiredMaterials;
|
std::map<std::string, int> requiredMaterials;
|
||||||
const ShipDef* shipDef = config.ships.findShipDef(b.recipeId);
|
const ShipDef* shipDef = config.ships.findShipDef(recipeId);
|
||||||
if (!shipDef)
|
if (!shipDef)
|
||||||
{
|
{
|
||||||
return requiredMaterials;
|
return requiredMaterials;
|
||||||
@@ -56,9 +64,9 @@ computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
|||||||
{
|
{
|
||||||
requiredMaterials[ing.item] += ing.amount;
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
}
|
}
|
||||||
if (b.shipLayout.has_value())
|
if (shipLayout.has_value())
|
||||||
{
|
{
|
||||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
for (const PlacedModule& pm : shipLayout->placedModules)
|
||||||
{
|
{
|
||||||
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||||
if (!modDef)
|
if (!modDef)
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
|
|||||||
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
|
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
|
||||||
const Building& b);
|
const Building& b);
|
||||||
|
|
||||||
|
// The same sum over a stored configuration rather than an operational building, so a
|
||||||
|
// construction site's schematic can be costed before it is built (REQ-BLD-SITE-CONFIG).
|
||||||
|
std::map<std::string, int> computeShipyardRequiredMaterials(
|
||||||
|
const GameConfig& config, const std::string& recipeId,
|
||||||
|
const std::optional<ShipLayoutConfig>& shipLayout);
|
||||||
|
|
||||||
// True when a production cycle could start right now, ignoring output-buffer space.
|
// True when a production cycle could start right now, ignoring output-buffer space.
|
||||||
bool hasInputsToStart(const GameConfig& config, const Building& b);
|
bool hasInputsToStart(const GameConfig& config, const Building& b);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#include "ShipStatsPanel.h"
|
#include "ShipStatsPanel.h"
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BarRow.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "SectionBox.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
|
#include "StatRow.h"
|
||||||
#include "ThreatCostCalculator.h"
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -16,20 +19,6 @@ QString fmt(float value)
|
|||||||
return QString::number(static_cast<double>(value), 'f', 1);
|
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
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -38,215 +27,150 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
|||||||
, m_config(config)
|
, m_config(config)
|
||||||
{
|
{
|
||||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(4, 4, 4, 4);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->setSpacing(2);
|
layout->setSpacing(2);
|
||||||
layout->setAlignment(Qt::AlignTop);
|
layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
// Hull stats — always visible.
|
// Hull stats -- always visible, except cargo capacity, which only a ship that can
|
||||||
m_hpLabel = makeStatLabel(this);
|
// carry anything shows (REQ-MOD-UI-STATS-PANEL).
|
||||||
m_speedLabel = makeStatLabel(this);
|
m_hpBar = new BarRow(tr("HP"), this);
|
||||||
m_sensorRangeLabel = makeStatLabel(this);
|
m_speedRow = new StatRow(tr("Max speed"), this);
|
||||||
m_mainAccelLabel = makeStatLabel(this);
|
m_sensorRangeRow = new StatRow(tr("Sensor range"), this);
|
||||||
m_maneuveringAccelLabel = makeStatLabel(this);
|
m_mainAccelRow = new StatRow(tr("Main accel"), this);
|
||||||
m_angularAccelLabel = makeStatLabel(this);
|
m_maneuveringAccelRow = new StatRow(tr("Maneuvering accel"), this);
|
||||||
m_maxRotSpeedLabel = makeStatLabel(this);
|
m_angularAccelRow = new StatRow(tr("Angular accel"), this);
|
||||||
m_cargoCapacityLabel = makeStatLabel(this);
|
m_maxRotSpeedRow = new StatRow(tr("Max rotation"), this);
|
||||||
m_cargoCapacityLabel->setVisible(false);
|
m_cargoCapacityRow = new StatRow(tr("Cargo capacity"), this);
|
||||||
|
m_cargoCapacityRow->hide();
|
||||||
|
|
||||||
layout->addWidget(m_hpLabel);
|
layout->addWidget(m_hpBar);
|
||||||
layout->addWidget(m_speedLabel);
|
layout->addWidget(m_speedRow);
|
||||||
layout->addWidget(m_sensorRangeLabel);
|
layout->addWidget(m_sensorRangeRow);
|
||||||
layout->addWidget(m_mainAccelLabel);
|
layout->addWidget(m_mainAccelRow);
|
||||||
layout->addWidget(m_maneuveringAccelLabel);
|
layout->addWidget(m_maneuveringAccelRow);
|
||||||
layout->addWidget(m_angularAccelLabel);
|
layout->addWidget(m_angularAccelRow);
|
||||||
layout->addWidget(m_maxRotSpeedLabel);
|
layout->addWidget(m_maxRotSpeedRow);
|
||||||
layout->addWidget(m_cargoCapacityLabel);
|
layout->addWidget(m_cargoCapacityRow);
|
||||||
|
|
||||||
// Weapon capability section.
|
// One section per capability module type, each shown only while at least one such
|
||||||
m_weaponSection = new QWidget(this);
|
// module is installed (REQ-MOD-UI-STATS-PANEL).
|
||||||
{
|
m_weaponSection = new SectionBox(tr("Weapons"), this);
|
||||||
QVBoxLayout* sl = new QVBoxLayout(m_weaponSection);
|
m_weaponDpsRow = new StatRow(tr("DPS"), m_weaponSection);
|
||||||
sl->setContentsMargins(0, 4, 0, 0);
|
m_weaponRangeRow = new StatRow(tr("Range"), m_weaponSection);
|
||||||
sl->setSpacing(2);
|
m_weaponSection->getContentLayout()->addWidget(m_weaponDpsRow);
|
||||||
sl->addWidget(makeSectionHeader(tr("Weapons"), m_weaponSection));
|
m_weaponSection->getContentLayout()->addWidget(m_weaponRangeRow);
|
||||||
m_weaponDpsLabel = makeStatLabel(m_weaponSection);
|
m_weaponSection->hide();
|
||||||
m_weaponRangeLabel = makeStatLabel(m_weaponSection);
|
|
||||||
sl->addWidget(m_weaponDpsLabel);
|
|
||||||
sl->addWidget(m_weaponRangeLabel);
|
|
||||||
}
|
|
||||||
m_weaponSection->setVisible(false);
|
|
||||||
layout->addWidget(m_weaponSection);
|
layout->addWidget(m_weaponSection);
|
||||||
|
|
||||||
// Salvage capability section.
|
m_salvageSection = new SectionBox(tr("Salvage"), this);
|
||||||
m_salvageSection = new QWidget(this);
|
m_salvageRateRow = new StatRow(tr("Collection rate"), m_salvageSection);
|
||||||
{
|
m_salvageRangeRow = new StatRow(tr("Range"), m_salvageSection);
|
||||||
QVBoxLayout* sl = new QVBoxLayout(m_salvageSection);
|
m_salvageSection->getContentLayout()->addWidget(m_salvageRateRow);
|
||||||
sl->setContentsMargins(0, 4, 0, 0);
|
m_salvageSection->getContentLayout()->addWidget(m_salvageRangeRow);
|
||||||
sl->setSpacing(2);
|
m_salvageSection->hide();
|
||||||
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);
|
layout->addWidget(m_salvageSection);
|
||||||
|
|
||||||
// Repair capability section.
|
m_repairSection = new SectionBox(tr("Repair"), this);
|
||||||
m_repairSection = new QWidget(this);
|
m_repairRateRow = new StatRow(tr("Repair rate"), m_repairSection);
|
||||||
{
|
m_repairRangeRow = new StatRow(tr("Range"), m_repairSection);
|
||||||
QVBoxLayout* sl = new QVBoxLayout(m_repairSection);
|
m_repairSection->getContentLayout()->addWidget(m_repairRateRow);
|
||||||
sl->setContentsMargins(0, 4, 0, 0);
|
m_repairSection->getContentLayout()->addWidget(m_repairRangeRow);
|
||||||
sl->setSpacing(2);
|
m_repairSection->hide();
|
||||||
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->addWidget(m_repairSection);
|
||||||
|
|
||||||
// Current behavior — live entities only; hidden in the static design
|
// Live entities only; the design preview has no behavior to show
|
||||||
// preview (REQ-UI-SHIP-BEHAVIOR).
|
// (REQ-UI-SHIP-BEHAVIOR).
|
||||||
m_behaviorLabel = makeSectionHeader(QString(), this);
|
m_behaviorRow = new StatRow(tr("Behavior"), this);
|
||||||
m_behaviorLabel->setVisible(false);
|
m_behaviorRow->hide();
|
||||||
layout->addWidget(m_behaviorLabel);
|
layout->addWidget(m_behaviorRow);
|
||||||
|
|
||||||
// Threat cost — debug-only, initially hidden.
|
// Threat cost -- shown only while debug draw is active (REQ-UI-SHIP-STATS-PANEL).
|
||||||
m_threatCostLabel = makeStatLabel(this);
|
m_threatCostRow = new StatRow(tr("Threat cost"), this);
|
||||||
m_threatCostLabel->setVisible(false);
|
m_threatCostRow->hide();
|
||||||
layout->addWidget(m_threatCostLabel);
|
layout->addWidget(m_threatCostRow);
|
||||||
|
|
||||||
layout->addStretch();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShipStatsPanel::refresh(const std::string& shipId,
|
|
||||||
const std::vector<PlacedModule>& modules)
|
|
||||||
{
|
|
||||||
const ShipStats stats = calculateShipStats(*m_config, shipId, modules);
|
|
||||||
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
|
|
||||||
applyStats(stats, hpText);
|
|
||||||
|
|
||||||
const double threat = calculateShipThreatCost(m_config->threatCosts, *m_config,
|
|
||||||
shipId, modules);
|
|
||||||
setThreatCost(threat);
|
|
||||||
|
|
||||||
// The static design preview has no live behavior to show.
|
|
||||||
m_behaviorLabel->setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
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)));
|
|
||||||
|
|
||||||
// Cargo capacity is shown only when the ship can actually hold cargo
|
|
||||||
// (REQ-MOD-UI-STATS-PANEL).
|
|
||||||
if (stats.cargoCapacity > 0)
|
|
||||||
{
|
|
||||||
m_cargoCapacityLabel->setText(
|
|
||||||
tr("Cargo Capacity: %1").arg(stats.cargoCapacity));
|
|
||||||
m_cargoCapacityLabel->setVisible(true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_cargoCapacityLabel->setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipStatsPanel::setBehavior(BehaviorKind kind)
|
void ShipStatsPanel::setBehavior(BehaviorKind kind)
|
||||||
{
|
{
|
||||||
QString label;
|
const QString label = getBehaviorLabel(kind);
|
||||||
switch (kind)
|
m_behaviorRow->setValue(label);
|
||||||
|
m_behaviorRow->setVisible(!label.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipStatsPanel::refresh(const std::string& shipId,
|
||||||
|
const std::vector<PlacedModule>& modules)
|
||||||
|
{
|
||||||
|
const ShipStats stats = calculateShipStats(*m_config, shipId, modules);
|
||||||
|
applyStats(stats, 1.0, QString::number(static_cast<int>(stats.hp + 0.5f)));
|
||||||
|
|
||||||
|
setThreatCost(calculateShipThreatCost(m_config->threatCosts, *m_config,
|
||||||
|
shipId, modules));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
|
||||||
|
{
|
||||||
|
const double fraction = (stats.hp > 0.0f)
|
||||||
|
? static_cast<double>(currentHp) / stats.hp
|
||||||
|
: 0.0;
|
||||||
|
applyStats(stats, fraction, tr("%1 / %2")
|
||||||
|
.arg(static_cast<int>(currentHp + 0.5f))
|
||||||
|
.arg(static_cast<int>(stats.hp + 0.5f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipStatsPanel::applyStats(const ShipStats& stats, double hpFraction,
|
||||||
|
const QString& hpText)
|
||||||
|
{
|
||||||
|
m_hpBar->setValue(hpFraction, hpText);
|
||||||
|
m_speedRow->setValue(tr("%1 tiles/s").arg(fmt(stats.maxSpeed_tps)));
|
||||||
|
m_sensorRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.sensorRange_tiles)));
|
||||||
|
m_mainAccelRow->setValue(
|
||||||
|
tr("%1 tiles/s\xc2\xb2").arg(fmt(stats.mainAcceleration_tpss)));
|
||||||
|
m_maneuveringAccelRow->setValue(
|
||||||
|
tr("%1 tiles/s\xc2\xb2").arg(fmt(stats.maneuveringAcceleration_tpss)));
|
||||||
|
m_angularAccelRow->setValue(
|
||||||
|
tr("%1 rad/s\xc2\xb2").arg(fmt(stats.angularAcceleration_radpss)));
|
||||||
|
m_maxRotSpeedRow->setValue(tr("%1 rad/s").arg(fmt(stats.maxRotationSpeed_radps)));
|
||||||
|
|
||||||
|
// Cargo capacity is shown only when the ship can actually hold cargo
|
||||||
|
// (REQ-MOD-UI-STATS-PANEL).
|
||||||
|
m_cargoCapacityRow->setVisible(stats.cargoCapacity > 0);
|
||||||
|
if (stats.cargoCapacity > 0)
|
||||||
{
|
{
|
||||||
case BehaviorKind::Retreat: label = tr("Retreating"); break;
|
m_cargoCapacityRow->setValue(QString::number(stats.cargoCapacity));
|
||||||
case BehaviorKind::Attack: label = tr("Engaging"); break;
|
|
||||||
case BehaviorKind::SalvageScrap:
|
|
||||||
case BehaviorKind::DeliverScrap: label = tr("Salvaging"); break;
|
|
||||||
case BehaviorKind::Repair: label = tr("Repairing"); break;
|
|
||||||
case BehaviorKind::Rally: label = tr("Rallying"); break;
|
|
||||||
case BehaviorKind::Standby: label = tr("Standby"); break;
|
|
||||||
case BehaviorKind::Advance: label = tr("Advancing"); break;
|
|
||||||
case BehaviorKind::None: break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (label.isEmpty())
|
m_weaponSection->setVisible(stats.weapons.has_value());
|
||||||
|
if (stats.weapons.has_value())
|
||||||
{
|
{
|
||||||
m_behaviorLabel->setVisible(false);
|
m_weaponDpsRow->setValue(fmt(stats.weapons->combinedDps));
|
||||||
return;
|
m_weaponRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.weapons->maxRange_tiles)));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_behaviorLabel->setText(tr("Behavior: %1").arg(label));
|
m_salvageSection->setVisible(stats.salvage.has_value());
|
||||||
m_behaviorLabel->setVisible(true);
|
if (stats.salvage.has_value())
|
||||||
|
{
|
||||||
|
m_salvageRateRow->setValue(
|
||||||
|
tr("%1 /s").arg(fmt(stats.salvage->combinedCollectionRate)));
|
||||||
|
m_salvageRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.salvage->maxRange_tiles)));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_repairSection->setVisible(stats.repair.has_value());
|
||||||
|
if (stats.repair.has_value())
|
||||||
|
{
|
||||||
|
m_repairRateRow->setValue(
|
||||||
|
tr("%1 HP/s").arg(fmt(stats.repair->combinedRepairRate_hps)));
|
||||||
|
m_repairRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.repair->maxRange_tiles)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipStatsPanel::setThreatCost(double cost)
|
void ShipStatsPanel::setThreatCost(double cost)
|
||||||
{
|
{
|
||||||
m_threatCostLabel->setText(tr("Threat Cost: %1").arg(cost, 0, 'f', 1));
|
m_threatCostRow->setValue(QString::number(cost, 'f', 1));
|
||||||
m_threatCostLabel->setVisible(m_debugDraw);
|
m_threatCostRow->setVisible(m_debugDraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipStatsPanel::setDebugDrawEnabled(bool enabled)
|
void ShipStatsPanel::setDebugDrawEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
m_debugDraw = enabled;
|
m_debugDraw = enabled;
|
||||||
m_threatCostLabel->setVisible(m_debugDraw);
|
m_threatCostRow->setVisible(m_debugDraw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
|
|
||||||
struct GameConfig;
|
#include "BehaviorKind.h"
|
||||||
class QLabel;
|
|
||||||
|
|
||||||
|
struct GameConfig;
|
||||||
|
class BarRow;
|
||||||
|
class SectionBox;
|
||||||
|
class StatRow;
|
||||||
|
|
||||||
|
// The hull stats and capability module summaries of one ship, as a bar for HP and a
|
||||||
|
// label/value row for everything else. Shared by the live selection card
|
||||||
|
// (REQ-UI-SHIP-STATS-PANEL), the layout configuration dialog's design preview
|
||||||
|
// (REQ-MOD-UI-STATS-PANEL) and the balancing tool, so all three read alike.
|
||||||
|
//
|
||||||
|
// The behavior row is for consumers with no header to put it in. The selection card
|
||||||
|
// shows the behavior in its header instead (REQ-UI-SHIP-BEHAVIOR) and leaves the row
|
||||||
|
// unset; the design preview has no live ship to have a behavior at all.
|
||||||
class ShipStatsPanel : public QWidget
|
class ShipStatsPanel : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -20,44 +30,47 @@ class ShipStatsPanel : public QWidget
|
|||||||
public:
|
public:
|
||||||
explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr);
|
explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// Stats of a design rather than of a live ship: the HP bar reads full, because the
|
||||||
|
// number shown is the maximum the design would have.
|
||||||
void refresh(const std::string& shipId,
|
void refresh(const std::string& shipId,
|
||||||
const std::vector<PlacedModule>& modules);
|
const std::vector<PlacedModule>& modules);
|
||||||
|
|
||||||
void refreshFromLive(const ShipStats& stats, float currentHp);
|
void refreshFromLive(const ShipStats& stats, float currentHp);
|
||||||
|
|
||||||
// Displays the ship's current top-priority behavior (REQ-UI-SHIP-BEHAVIOR).
|
// Shows the ship's top-priority behavior as a row of its own. Never called by the
|
||||||
|
// selection card, which has a header slot for it.
|
||||||
void setBehavior(BehaviorKind kind);
|
void setBehavior(BehaviorKind kind);
|
||||||
|
|
||||||
void setThreatCost(double cost);
|
void setThreatCost(double cost);
|
||||||
void setDebugDrawEnabled(bool enabled);
|
void setDebugDrawEnabled(bool enabled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void applyStats(const ShipStats& stats, const QString& hpText);
|
void applyStats(const ShipStats& stats, double hpFraction, const QString& hpText);
|
||||||
|
|
||||||
const GameConfig* m_config;
|
const GameConfig* m_config;
|
||||||
bool m_debugDraw = false;
|
bool m_debugDraw = false;
|
||||||
|
|
||||||
QLabel* m_behaviorLabel;
|
BarRow* m_hpBar;
|
||||||
QLabel* m_hpLabel;
|
StatRow* m_speedRow;
|
||||||
QLabel* m_speedLabel;
|
StatRow* m_sensorRangeRow;
|
||||||
QLabel* m_sensorRangeLabel;
|
StatRow* m_mainAccelRow;
|
||||||
QLabel* m_mainAccelLabel;
|
StatRow* m_maneuveringAccelRow;
|
||||||
QLabel* m_maneuveringAccelLabel;
|
StatRow* m_angularAccelRow;
|
||||||
QLabel* m_angularAccelLabel;
|
StatRow* m_maxRotSpeedRow;
|
||||||
QLabel* m_maxRotSpeedLabel;
|
StatRow* m_cargoCapacityRow;
|
||||||
QLabel* m_cargoCapacityLabel;
|
|
||||||
|
|
||||||
QWidget* m_weaponSection;
|
SectionBox* m_weaponSection;
|
||||||
QLabel* m_weaponDpsLabel;
|
StatRow* m_weaponDpsRow;
|
||||||
QLabel* m_weaponRangeLabel;
|
StatRow* m_weaponRangeRow;
|
||||||
|
|
||||||
QWidget* m_salvageSection;
|
SectionBox* m_salvageSection;
|
||||||
QLabel* m_salvageRateLabel;
|
StatRow* m_salvageRateRow;
|
||||||
QLabel* m_salvageRangeLabel;
|
StatRow* m_salvageRangeRow;
|
||||||
|
|
||||||
QWidget* m_repairSection;
|
SectionBox* m_repairSection;
|
||||||
QLabel* m_repairRateLabel;
|
StatRow* m_repairRateRow;
|
||||||
QLabel* m_repairRangeLabel;
|
StatRow* m_repairRangeRow;
|
||||||
|
|
||||||
QLabel* m_threatCostLabel;
|
StatRow* m_behaviorRow;
|
||||||
|
StatRow* m_threatCostRow;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "SelectionNames.h"
|
|
||||||
|
|
||||||
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request,
|
const SelectionRequest& request,
|
||||||
@@ -12,31 +11,21 @@ AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutoProductionContent::refreshConfiguration()
|
|
||||||
{
|
|
||||||
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
|
||||||
if (!target.isValid())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
||||||
}
|
|
||||||
|
|
||||||
BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
|
BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
|
||||||
const Building& building) const
|
const BuildingTarget& target) const
|
||||||
{
|
{
|
||||||
CycleInfo info;
|
CycleInfo info;
|
||||||
// An auto-recipe building always runs an implicit recipe (REQ-BLD-SMELTER,
|
// An auto-recipe building always runs an implicit recipe (REQ-BLD-SMELTER,
|
||||||
// REQ-BLD-REPROCESSING), so its production section is always shown -- but only a
|
// REQ-BLD-REPROCESSING), so its production section is always shown -- but only a
|
||||||
// running cycle names a recipe, so while it is idle there is no cycle time.
|
// running cycle names a recipe, so while it is idle there is no cycle to describe.
|
||||||
info.runsProduction = true;
|
info.runsProduction = true;
|
||||||
if (!building.production.has_value())
|
if (!target.building || !target.building->production.has_value())
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
|
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
|
||||||
building.production->recipeId, building.type);
|
target.building->production->recipeId, target.type);
|
||||||
if (!recipe)
|
if (!recipe)
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
|
|||||||
@@ -16,6 +16,5 @@ public:
|
|||||||
const SelectionRequest& request, QWidget* parent = nullptr);
|
const SelectionRequest& request, QWidget* parent = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void refreshConfiguration() override;
|
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||||
CycleInfo getCycleInfo(const Building& building) const override;
|
|
||||||
};
|
};
|
||||||
|
|||||||
118
src/ui/selection/BarRow.cpp
Normal file
118
src/ui/selection/BarRow.cpp
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#include "BarRow.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Height and corner radius of the fill bar, in device-independent pixels.
|
||||||
|
const int kBarHeightPx = 6;
|
||||||
|
const qreal kBarRadiusPx = 3.0;
|
||||||
|
|
||||||
|
// Opacity of the unfilled track, over the card's background.
|
||||||
|
const int kTrackAlpha = 60;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
// The bar itself. Painted rather than assembled from a QProgressBar, because all it
|
||||||
|
// needs is a rounded track with a rounded fill and a style sheet cannot be relied on to
|
||||||
|
// leave a progress bar's groove and chunk alone across styles.
|
||||||
|
class BarRow::Bar : public QWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Bar(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_fraction(0.0)
|
||||||
|
{
|
||||||
|
setFixedHeight(kBarHeightPx);
|
||||||
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFraction(double fraction)
|
||||||
|
{
|
||||||
|
m_fraction = std::max(0.0, std::min(1.0, fraction));
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFillColor(const QColor& color)
|
||||||
|
{
|
||||||
|
m_fillColor = color;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* /*event*/) override
|
||||||
|
{
|
||||||
|
const QColor fill = m_fillColor.isValid()
|
||||||
|
? m_fillColor
|
||||||
|
: palette().color(QPalette::Highlight);
|
||||||
|
|
||||||
|
QColor track = fill;
|
||||||
|
track.setAlpha(kTrackAlpha);
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
|
||||||
|
painter.setBrush(track);
|
||||||
|
painter.drawRoundedRect(rect(), kBarRadiusPx, kBarRadiusPx);
|
||||||
|
|
||||||
|
if (m_fraction > 0.0)
|
||||||
|
{
|
||||||
|
QRect filled = rect();
|
||||||
|
filled.setWidth(static_cast<int>(filled.width() * m_fraction));
|
||||||
|
painter.setBrush(fill);
|
||||||
|
painter.drawRoundedRect(filled, kBarRadiusPx, kBarRadiusPx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double m_fraction;
|
||||||
|
QColor m_fillColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
BarRow::BarRow(const QString& caption, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(2);
|
||||||
|
|
||||||
|
QWidget* captionRow = new QWidget(this);
|
||||||
|
QHBoxLayout* captionLayout = new QHBoxLayout(captionRow);
|
||||||
|
captionLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
captionLayout->setSpacing(8);
|
||||||
|
|
||||||
|
m_captionLabel = new QLabel(caption, captionRow);
|
||||||
|
m_captionLabel->setVisible(!caption.isEmpty());
|
||||||
|
m_valueLabel = new QLabel(captionRow);
|
||||||
|
m_valueLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
|
||||||
|
captionLayout->addWidget(m_captionLabel);
|
||||||
|
captionLayout->addStretch(1);
|
||||||
|
captionLayout->addWidget(m_valueLabel);
|
||||||
|
|
||||||
|
m_bar = new Bar(this);
|
||||||
|
|
||||||
|
layout->addWidget(captionRow);
|
||||||
|
layout->addWidget(m_bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BarRow::setValue(double fraction, const QString& valueText)
|
||||||
|
{
|
||||||
|
m_bar->setFraction(fraction);
|
||||||
|
m_valueLabel->setText(valueText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BarRow::setFillColor(const QColor& color)
|
||||||
|
{
|
||||||
|
m_bar->setFillColor(color);
|
||||||
|
}
|
||||||
36
src/ui/selection/BarRow.h
Normal file
36
src/ui/selection/BarRow.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// A caption with its value hard right and a horizontal fill bar beneath it. One part for
|
||||||
|
// the three things the panel shows as a proportion: a construction site's progress, a
|
||||||
|
// building's production cycle, and the HP of a ship, a station or the HQ
|
||||||
|
// (REQ-UI-PRODUCTION-PROGRESS, REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL,
|
||||||
|
// REQ-UI-HQ-PANEL).
|
||||||
|
//
|
||||||
|
// The caption may be left empty, for a bar whose meaning is already given by the section
|
||||||
|
// it sits in.
|
||||||
|
class BarRow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BarRow(const QString& caption, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// fraction is clamped to [0, 1]; valueText is shown beside the caption as-is, so a
|
||||||
|
// bar can read "72%" or "340 / 500" as its meaning requires.
|
||||||
|
void setValue(double fraction, const QString& valueText);
|
||||||
|
// Overrides the fill color, which defaults to the palette's highlight.
|
||||||
|
void setFillColor(const QColor& color);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Bar;
|
||||||
|
|
||||||
|
QLabel* m_captionLabel;
|
||||||
|
QLabel* m_valueLabel;
|
||||||
|
Bar* m_bar;
|
||||||
|
};
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
#include "BufferSection.h"
|
#include "BufferSection.h"
|
||||||
|
|
||||||
#include <QLabel>
|
#include <vector>
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "ItemChipRow.h"
|
||||||
|
#include "SectionBox.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
// One "<item>: <count>[/<per cycle>]" entry.
|
|
||||||
QString formatEntry(const std::string& itemId, int count, int perCycle)
|
|
||||||
{
|
|
||||||
QString text = QString::fromStdString(itemId) + ": " + QString::number(count);
|
|
||||||
if (perCycle > 0)
|
|
||||||
{
|
|
||||||
text += "/" + QString::number(perCycle);
|
|
||||||
}
|
|
||||||
return text + " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
int findPerCycle(const std::map<std::string, int>& perCycle, const std::string& itemId)
|
int findPerCycle(const std::map<std::string, int>& perCycle, const std::string& itemId)
|
||||||
{
|
{
|
||||||
const std::map<std::string, int>::const_iterator it = perCycle.find(itemId);
|
const std::map<std::string, int>::const_iterator it = perCycle.find(itemId);
|
||||||
@@ -28,33 +21,42 @@ int findPerCycle(const std::map<std::string, int>& perCycle, const std::string&
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
BufferSection::BufferSection(QWidget* parent)
|
BufferSection::BufferSection(const SelectionContext& context, QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->setSpacing(0);
|
layout->setSpacing(6);
|
||||||
|
|
||||||
m_label = new QLabel(this);
|
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
||||||
m_label->setWordWrap(true);
|
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
|
||||||
layout->addWidget(m_label);
|
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
||||||
|
|
||||||
|
m_outputSection = new SectionBox(tr("Output buffer"), this);
|
||||||
|
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
|
||||||
|
m_outputSection->getContentLayout()->addWidget(m_outputChips);
|
||||||
|
|
||||||
|
layout->addWidget(m_inputSection);
|
||||||
|
layout->addWidget(m_outputSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferSection::setBuffers(const Building& building,
|
void BufferSection::setBuffers(const Building& building,
|
||||||
const std::map<std::string, int>& perCycleInputs,
|
const std::map<std::string, int>& perCycleInputs,
|
||||||
const std::map<std::string, int>& perCycleOutputs)
|
const std::map<std::string, int>& perCycleOutputs)
|
||||||
{
|
{
|
||||||
QString text;
|
std::vector<ItemChipRow::Entry> inputs;
|
||||||
|
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
||||||
if (!building.inputBuffer.counts.empty())
|
|
||||||
{
|
{
|
||||||
text += tr("Input: ");
|
ItemChipRow::Entry chip;
|
||||||
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
chip.itemId = entry.first.id;
|
||||||
|
chip.countText = QString::number(entry.second);
|
||||||
|
|
||||||
|
const int perCycle = findPerCycle(perCycleInputs, entry.first.id);
|
||||||
|
if (perCycle > 0)
|
||||||
{
|
{
|
||||||
text += formatEntry(entry.first.id, entry.second,
|
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
|
||||||
findPerCycle(perCycleInputs, entry.first.id));
|
|
||||||
}
|
}
|
||||||
text += "\n";
|
inputs.push_back(chip);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output-side items are the buffered ones plus those still emerging onto the output
|
// Output-side items are the buffered ones plus those still emerging onto the output
|
||||||
@@ -72,24 +74,30 @@ void BufferSection::setBuffers(const Building& building,
|
|||||||
outputCounts[slot.item.type.id]++;
|
outputCounts[slot.item.type.id]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A configured building lists everything its cycle produces, so an output the player
|
||||||
// A configured building lists every item its cycle produces, so an output the player
|
// is waiting for reads as 0 rather than being absent.
|
||||||
// is waiting for shows as 0 rather than being absent.
|
|
||||||
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
|
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
|
||||||
{
|
{
|
||||||
outputCounts.emplace(entry.first, 0);
|
outputCounts.emplace(entry.first, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outputCounts.empty())
|
std::vector<ItemChipRow::Entry> outputs;
|
||||||
|
for (const std::pair<const std::string, int>& entry : outputCounts)
|
||||||
{
|
{
|
||||||
text += tr("Output: ");
|
ItemChipRow::Entry chip;
|
||||||
for (const std::pair<const std::string, int>& entry : outputCounts)
|
chip.itemId = entry.first;
|
||||||
{
|
// Counted against the buffer's capacity, which is what production stops at
|
||||||
text += formatEntry(entry.first, entry.second,
|
// (REQ-MAT-OUTPUT-BUFFER).
|
||||||
findPerCycle(perCycleOutputs, entry.first));
|
chip.countText = building.outputBuffer.capacity > 0
|
||||||
}
|
? tr("%1 / %2").arg(entry.second).arg(building.outputBuffer.capacity)
|
||||||
|
: QString::number(entry.second);
|
||||||
|
chip.subLine = QString::fromStdString(toDisplayName(entry.first));
|
||||||
|
outputs.push_back(chip);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_label->setText(text.trimmed());
|
m_inputChips->setEntries(inputs);
|
||||||
setVisible(!text.trimmed().isEmpty());
|
m_outputChips->setEntries(outputs);
|
||||||
|
m_inputSection->setVisible(!inputs.empty());
|
||||||
|
m_outputSection->setVisible(!outputs.empty());
|
||||||
|
setVisible(!inputs.empty() || !outputs.empty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,25 @@
|
|||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
struct Building;
|
#include "SelectionContext.h"
|
||||||
class QLabel;
|
|
||||||
|
|
||||||
// The input and output buffer contents of one building (REQ-UI-SINGLE-SELECTION).
|
struct Building;
|
||||||
|
class ItemChipRow;
|
||||||
|
class SectionBox;
|
||||||
|
|
||||||
|
// The input and output buffer contents of one building, each a captioned section of item
|
||||||
|
// chips (REQ-UI-SINGLE-SELECTION).
|
||||||
//
|
//
|
||||||
// Counting what is in the buffers is the same for every building type, so it happens
|
// Counting what is in the buffers is the same for every building type, so it happens
|
||||||
// here; what a cycle consumes and produces is not, so the owning content supplies those
|
// here; what a cycle consumes and produces is not, so the owning content supplies those
|
||||||
// per-cycle amounts. An item with no entry in the maps is shown without a denominator.
|
// per-cycle amounts. An item with no entry in the maps is shown without a denominator,
|
||||||
|
// and a section holding nothing is not shown at all.
|
||||||
class BufferSection : public QWidget
|
class BufferSection : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit BufferSection(QWidget* parent = nullptr);
|
explicit BufferSection(const SelectionContext& context, QWidget* parent = nullptr);
|
||||||
|
|
||||||
// perCycleInputs and perCycleOutputs map an item id to the amount one production
|
// perCycleInputs and perCycleOutputs map an item id to the amount one production
|
||||||
// cycle consumes or produces. Both may be empty, for a building that runs no cycle.
|
// cycle consumes or produces. Both may be empty, for a building that runs no cycle.
|
||||||
@@ -27,5 +32,8 @@ public:
|
|||||||
const std::map<std::string, int>& perCycleOutputs);
|
const std::map<std::string, int>& perCycleOutputs);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel* m_label;
|
SectionBox* m_inputSection;
|
||||||
|
ItemChipRow* m_inputChips;
|
||||||
|
SectionBox* m_outputSection;
|
||||||
|
ItemChipRow* m_outputChips;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "BufferedBuildingContent.h"
|
#include "BufferedBuildingContent.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
@@ -7,33 +9,76 @@
|
|||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "ProductionSection.h"
|
#include "ProductionSection.h"
|
||||||
|
#include "RecipeSummaryRow.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::vector<RecipeSummaryRow::Amount> toAmounts(const std::map<std::string, int>& map)
|
||||||
|
{
|
||||||
|
std::vector<RecipeSummaryRow::Amount> amounts;
|
||||||
|
amounts.reserve(map.size());
|
||||||
|
for (const std::pair<const std::string, int>& entry : map)
|
||||||
|
{
|
||||||
|
amounts.push_back(RecipeSummaryRow::Amount{ entry.first, entry.second });
|
||||||
|
}
|
||||||
|
return amounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
|
BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
|
||||||
BuildingId id, QWidget* parent)
|
BuildingId id, QWidget* parent)
|
||||||
: SelectionContent(context, asConstructionSite(context, id), parent)
|
: SelectionContent(context, asConstructionSite(context, id), parent)
|
||||||
, m_id(id)
|
, m_id(id)
|
||||||
{
|
{
|
||||||
m_buffers = new BufferSection(this);
|
// The summary is configuration -- what the building will do -- so it sits with the
|
||||||
|
// selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
|
||||||
|
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this);
|
||||||
|
getConfigurationLayout()->addWidget(m_recipeSummary);
|
||||||
|
|
||||||
|
m_buffers = new BufferSection(context, this);
|
||||||
m_production = new ProductionSection(this);
|
m_production = new ProductionSection(this);
|
||||||
getRuntimeLayout()->addWidget(m_buffers);
|
getRuntimeLayout()->addWidget(m_buffers);
|
||||||
getRuntimeLayout()->addWidget(m_production);
|
getRuntimeLayout()->addWidget(m_production);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferedBuildingContent::refreshRuntime()
|
void BufferedBuildingContent::refreshConfiguration()
|
||||||
{
|
{
|
||||||
const Building* building = findBuilding(getContext().sim->getFactoryState(), m_id);
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
||||||
if (!building)
|
if (!target.isValid())
|
||||||
{
|
{
|
||||||
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
|
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
|
||||||
// has to avoid reading it.
|
// has to avoid reading it.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setProductionStatusSlot(*building);
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
|
||||||
const CycleInfo cycle = getCycleInfo(*building);
|
const CycleInfo cycle = getCycleInfo(target);
|
||||||
m_buffers->setBuffers(*building, cycle.perCycleInputs, cycle.perCycleOutputs);
|
m_recipeSummary->setSummary(toAmounts(cycle.perCycleInputs),
|
||||||
m_production->setProduction(cycle.runsProduction, *building, cycle.durationSeconds,
|
toAmounts(cycle.perCycleOutputs),
|
||||||
|
cycle.durationSeconds);
|
||||||
|
|
||||||
|
refreshControls(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferedBuildingContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
||||||
|
if (!target.building)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProductionStatusSlot(*target.building);
|
||||||
|
|
||||||
|
const CycleInfo cycle = getCycleInfo(target);
|
||||||
|
m_buffers->setBuffers(*target.building, cycle.perCycleInputs, cycle.perCycleOutputs);
|
||||||
|
m_production->setProduction(cycle.runsProduction, *target.building,
|
||||||
|
cycle.durationSeconds,
|
||||||
getContext().sim->getCurrentTick());
|
getContext().sim->getCurrentTick());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,16 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
|
|
||||||
struct Building;
|
struct BuildingTarget;
|
||||||
class BufferSection;
|
class BufferSection;
|
||||||
class ProductionSection;
|
class ProductionSection;
|
||||||
|
class RecipeSummaryRow;
|
||||||
|
|
||||||
// Shared body of the four cards that show one building with buffers -- the Miner and
|
// Shared body of the four cards that show one building with buffers -- the Miner and
|
||||||
// Assembler, the Smelter and Reprocessing Plant, the Shipyard, and the Salvage Bay
|
// Assembler, the Smelter and Reprocessing Plant, the Shipyard, and the Salvage Bay
|
||||||
// (REQ-UI-SELECTION-CONTENT). All four show the same header status, buffer contents and
|
// (REQ-UI-SELECTION-CONTENT). All four show the same header identity and status, recipe
|
||||||
// production progress; they differ only in what one production cycle costs and how long
|
// summary, buffer contents and production progress; they differ only in what one
|
||||||
// it takes, which is what the subclass supplies.
|
// production cycle costs and how long it takes, which is what the subclass supplies.
|
||||||
//
|
//
|
||||||
// This is implementation sharing, not a catalog entry: every concrete subclass is one
|
// This is implementation sharing, not a catalog entry: every concrete subclass is one
|
||||||
// row of the content catalog.
|
// row of the content catalog.
|
||||||
@@ -40,14 +41,22 @@ protected:
|
|||||||
BufferedBuildingContent(const SelectionContext& context, BuildingId id,
|
BufferedBuildingContent(const SelectionContext& context, BuildingId id,
|
||||||
QWidget* parent);
|
QWidget* parent);
|
||||||
|
|
||||||
virtual CycleInfo getCycleInfo(const Building& building) const = 0;
|
// Called with a construction site's stored configuration too, so the summary of what
|
||||||
|
// the building will produce is shown before it is built (REQ-BLD-SITE-CONFIG).
|
||||||
|
virtual CycleInfo getCycleInfo(const BuildingTarget& target) const = 0;
|
||||||
|
|
||||||
|
// The subclass's own configuration controls. The identity, the recipe summary, the
|
||||||
|
// buffers and the production progress are handled here.
|
||||||
|
virtual void refreshControls(const BuildingTarget& /*target*/) {}
|
||||||
|
|
||||||
BuildingId getBuildingId() const { return m_id; }
|
BuildingId getBuildingId() const { return m_id; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void refreshConfiguration() override;
|
||||||
void refreshRuntime() override;
|
void refreshRuntime() override;
|
||||||
|
|
||||||
private:
|
|
||||||
BuildingId m_id;
|
BuildingId m_id;
|
||||||
|
RecipeSummaryRow* m_recipeSummary;
|
||||||
BufferSection* m_buffers;
|
BufferSection* m_buffers;
|
||||||
ProductionSection* m_production;
|
ProductionSection* m_production;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DebrisScrap.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DebrisScrap.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StatRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BarRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SectionBox.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/CountRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StatusPill.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
|
||||||
@@ -33,6 +42,15 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DebrisScrap.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DebrisScrap.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StatRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BarRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SectionBox.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/CountRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StatusPill.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
|
||||||
|
|||||||
29
src/ui/selection/CountRow.cpp
Normal file
29
src/ui/selection/CountRow.cpp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#include "CountRow.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
|
||||||
|
CountRow::CountRow(const QPixmap& symbol, const QString& name, int count,
|
||||||
|
QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(6);
|
||||||
|
|
||||||
|
m_symbolLabel = new QLabel(this);
|
||||||
|
m_symbolLabel->setPixmap(symbol);
|
||||||
|
m_symbolLabel->setVisible(!symbol.isNull());
|
||||||
|
|
||||||
|
m_nameLabel = new QLabel(name, this);
|
||||||
|
|
||||||
|
// The same "x<count>" notation the recipe tooltip and the header's aggregate count
|
||||||
|
// use (REQ-UI-MULTI-SELECTION).
|
||||||
|
m_countLabel = new QLabel(tr("x%1").arg(count), this);
|
||||||
|
m_countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
|
||||||
|
layout->addWidget(m_symbolLabel);
|
||||||
|
layout->addWidget(m_nameLabel);
|
||||||
|
layout->addStretch(1);
|
||||||
|
layout->addWidget(m_countLabel);
|
||||||
|
}
|
||||||
25
src/ui/selection/CountRow.h
Normal file
25
src/ui/selection/CountRow.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// One "<symbol> <name> x<count>" line of a count summary. The same part serves the
|
||||||
|
// building summary and the field summary, which count different things the same way
|
||||||
|
// (REQ-UI-MULTI-SELECTION, REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
class CountRow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
// An empty symbol leaves the icon off, for the kinds of object that have none.
|
||||||
|
CountRow(const QPixmap& symbol, const QString& name, int count,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_symbolLabel;
|
||||||
|
QLabel* m_nameLabel;
|
||||||
|
QLabel* m_countLabel;
|
||||||
|
};
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
#include "DebrisContent.h"
|
#include "DebrisContent.h"
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "DebrisScrap.h"
|
#include "DebrisScrap.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "StatRow.h"
|
||||||
|
|
||||||
DebrisContent::DebrisContent(const SelectionContext& context,
|
DebrisContent::DebrisContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request, QWidget* parent)
|
const SelectionRequest& request, QWidget* parent)
|
||||||
: SelectionContent(context, std::nullopt, parent)
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
, m_debris(request.debris)
|
, m_debris(request.debris)
|
||||||
{
|
{
|
||||||
m_scrapLabel = new QLabel(this);
|
m_scrapRow = new StatRow(tr("Scrap remaining"), this);
|
||||||
getRuntimeLayout()->addWidget(m_scrapLabel);
|
m_scrapRow->setValueEmphasized(true);
|
||||||
|
getRuntimeLayout()->addWidget(m_scrapRow);
|
||||||
|
|
||||||
setIdentity(QPixmap(), tr("Debris"));
|
setIdentity(QPixmap(), tr("Debris"));
|
||||||
if (m_debris.size() > 1)
|
if (m_debris.size() > 1)
|
||||||
@@ -24,7 +25,8 @@ DebrisContent::DebrisContent(const SelectionContext& context,
|
|||||||
void DebrisContent::refreshRuntime()
|
void DebrisContent::refreshRuntime()
|
||||||
{
|
{
|
||||||
// The value falls as the debris is collected and as pieces despawn
|
// The value falls as the debris is collected and as pieces despawn
|
||||||
// (REQ-UI-DEBRIS-CLICK-SELECT), so it is re-summed rather than remembered.
|
// (REQ-UI-DEBRIS-CLICK-SELECT), so it is re-summed rather than remembered. With
|
||||||
m_scrapLabel->setText(tr("Scrap remaining: %1")
|
// several pieces selected it is their sum (REQ-UI-SELECTION-AGGREGATE).
|
||||||
.arg(sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
|
m_scrapRow->setValue(QString::number(
|
||||||
|
sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
class QLabel;
|
class StatRow;
|
||||||
|
|
||||||
// The card for selected debris (REQ-UI-DEBRIS-PANEL): the scrap still left in it.
|
// The card for selected debris (REQ-UI-DEBRIS-PANEL): the scrap still left in it.
|
||||||
//
|
//
|
||||||
@@ -27,5 +27,5 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<entt::entity> m_debris;
|
std::vector<entt::entity> m_debris;
|
||||||
QLabel* m_scrapLabel;
|
StatRow* m_scrapRow;
|
||||||
};
|
};
|
||||||
|
|||||||
19
src/ui/selection/EmptyNote.cpp
Normal file
19
src/ui/selection/EmptyNote.cpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include "EmptyNote.h"
|
||||||
|
|
||||||
|
#include <QFont>
|
||||||
|
#include <QPalette>
|
||||||
|
|
||||||
|
EmptyNote::EmptyNote(const QString& text, QWidget* parent)
|
||||||
|
: QLabel(text, parent)
|
||||||
|
{
|
||||||
|
setWordWrap(true);
|
||||||
|
|
||||||
|
QFont noteFont = font();
|
||||||
|
noteFont.setItalic(true);
|
||||||
|
setFont(noteFont);
|
||||||
|
|
||||||
|
QPalette notePalette = palette();
|
||||||
|
notePalette.setColor(QPalette::WindowText,
|
||||||
|
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||||
|
setPalette(notePalette);
|
||||||
|
}
|
||||||
15
src/ui/selection/EmptyNote.h
Normal file
15
src/ui/selection/EmptyNote.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QLabel>
|
||||||
|
|
||||||
|
// A dimmed aside explaining why a part of the card is not there yet -- a construction
|
||||||
|
// site's "no buffers until built" (REQ-UI-SELECTION-CARD). Styled apart from the card's
|
||||||
|
// values so it reads as an explanation rather than as data.
|
||||||
|
class EmptyNote : public QLabel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit EmptyNote(const QString& text, QWidget* parent = nullptr);
|
||||||
|
};
|
||||||
@@ -3,33 +3,31 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "CountRow.h"
|
||||||
#include "DebrisScrap.h"
|
#include "DebrisScrap.h"
|
||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "StatRow.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
|
|
||||||
FieldMultiContent::FieldMultiContent(const SelectionContext& context,
|
FieldMultiContent::FieldMultiContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request, QWidget* parent)
|
const SelectionRequest& request, QWidget* parent)
|
||||||
: SelectionContent(context, std::nullopt, parent)
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
, m_actors(request.actors)
|
|
||||||
, m_debris(request.debris)
|
, m_debris(request.debris)
|
||||||
|
, m_scrapRow(nullptr)
|
||||||
{
|
{
|
||||||
m_summaryLabel = new QLabel(this);
|
|
||||||
m_summaryLabel->setWordWrap(true);
|
|
||||||
getRuntimeLayout()->addWidget(m_summaryLabel);
|
|
||||||
|
|
||||||
setIdentity(QPixmap(), tr("Mixed selection"));
|
setIdentity(QPixmap(), tr("Mixed selection"));
|
||||||
setCountSlot(static_cast<int>(m_actors.size() + m_debris.size()));
|
setCountSlot(static_cast<int>(request.actors.size() + request.debris.size()));
|
||||||
|
|
||||||
|
buildSummary(request.actors);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FieldMultiContent::refreshRuntime()
|
void FieldMultiContent::buildSummary(const std::vector<entt::entity>& actors)
|
||||||
{
|
{
|
||||||
EntityAdmin& admin = getContext().sim->getAdmin();
|
EntityAdmin& admin = getContext().sim->getAdmin();
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@ void FieldMultiContent::refreshRuntime()
|
|||||||
std::map<QString, int> counts;
|
std::map<QString, int> counts;
|
||||||
std::map<QString, QString> labels;
|
std::map<QString, QString> labels;
|
||||||
|
|
||||||
for (entt::entity actor : m_actors)
|
for (entt::entity actor : actors)
|
||||||
{
|
{
|
||||||
if (!admin.isValid(actor)) { continue; }
|
if (!admin.isValid(actor)) { continue; }
|
||||||
const bool isEnemy = admin.hasAll<FactionComponent>(actor)
|
const bool isEnemy = admin.hasAll<FactionComponent>(actor)
|
||||||
@@ -75,17 +73,33 @@ void FieldMultiContent::refreshRuntime()
|
|||||||
counts[key] += 1;
|
counts[key] += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList lines;
|
|
||||||
for (const QString& key : keys)
|
for (const QString& key : keys)
|
||||||
{
|
{
|
||||||
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
|
getRuntimeLayout()->addWidget(
|
||||||
|
new CountRow(QPixmap(), labels[key], counts[key], this));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_debris.empty())
|
if (!m_debris.empty())
|
||||||
{
|
{
|
||||||
lines << tr("Debris x %1").arg(static_cast<int>(m_debris.size()));
|
getRuntimeLayout()->addWidget(new CountRow(
|
||||||
// The scrap total follows the debris row rather than standing on its own, so it
|
QPixmap(), tr("Debris"), static_cast<int>(m_debris.size()), this));
|
||||||
// reads as belonging to it (REQ-UI-DEBRIS-PANEL).
|
|
||||||
lines << tr(" holding %1 scrap").arg(sumDebrisScrap(admin, m_debris));
|
// Indented under the debris row, so the total reads as belonging to it
|
||||||
|
// (REQ-UI-DEBRIS-PANEL).
|
||||||
|
m_scrapRow = new StatRow(tr("holding"), this);
|
||||||
|
m_scrapRow->setIndented(true);
|
||||||
|
m_scrapRow->setValueEmphasized(true);
|
||||||
|
getRuntimeLayout()->addWidget(m_scrapRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldMultiContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
// The counts are fixed for a given selection -- an actor leaving it re-publishes the
|
||||||
|
// selection and rebuilds this card -- but the scrap falls as the debris is collected.
|
||||||
|
if (m_scrapRow)
|
||||||
|
{
|
||||||
|
m_scrapRow->setValue(tr("%1 scrap")
|
||||||
|
.arg(sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
|
||||||
}
|
}
|
||||||
m_summaryLabel->setText(lines.join('\n'));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
class QLabel;
|
class StatRow;
|
||||||
|
|
||||||
// The count summary for a field selection holding more than one object that does not
|
// The count summary for a field selection holding more than one object that does not
|
||||||
// aggregate (REQ-UI-FIELD-MULTI-SELECTION): several actors, or actors together with
|
// aggregate (REQ-UI-FIELD-MULTI-SELECTION): several actors, or actors together with
|
||||||
@@ -24,7 +24,10 @@ protected:
|
|||||||
void refreshRuntime() override;
|
void refreshRuntime() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<entt::entity> m_actors;
|
void buildSummary(const std::vector<entt::entity>& actors);
|
||||||
|
|
||||||
std::vector<entt::entity> m_debris;
|
std::vector<entt::entity> m_debris;
|
||||||
QLabel* m_summaryLabel;
|
// Null unless debris is part of the selection; the only value here that changes
|
||||||
|
// while the selection stands.
|
||||||
|
StatRow* m_scrapRow;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
#include "HqContent.h"
|
#include "HqContent.h"
|
||||||
|
|
||||||
#include <QLabel>
|
#include <vector>
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BarRow.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
|
#include "IconCaption.h"
|
||||||
|
#include "ItemChipRow.h"
|
||||||
|
#include "SectionBox.h"
|
||||||
#include "SelectionNames.h"
|
#include "SelectionNames.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
@@ -15,18 +20,28 @@ HqContent::HqContent(const SelectionContext& context, const SelectionRequest& re
|
|||||||
// (REQ-BLD-DECONSTRUCT), so it is never a construction site.
|
// (REQ-BLD-DECONSTRUCT), so it is never a construction site.
|
||||||
: SelectionContent(context, std::nullopt, parent)
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
{
|
{
|
||||||
m_stockLabel = new QLabel(this);
|
m_stockSection = new SectionBox(tr("Building blocks"), this);
|
||||||
m_hpLabel = new QLabel(this);
|
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
|
||||||
getRuntimeLayout()->addWidget(m_stockLabel);
|
m_stockSection->getContentLayout()->addWidget(m_stockChips);
|
||||||
getRuntimeLayout()->addWidget(m_hpLabel);
|
|
||||||
|
m_hpBar = new BarRow(tr("HP"), this);
|
||||||
|
|
||||||
|
getRuntimeLayout()->addWidget(m_stockSection);
|
||||||
|
getRuntimeLayout()->addWidget(m_hpBar);
|
||||||
|
|
||||||
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
|
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
|
||||||
}
|
}
|
||||||
|
|
||||||
void HqContent::refreshRuntime()
|
void HqContent::refreshRuntime()
|
||||||
{
|
{
|
||||||
m_stockLabel->setText(
|
// Not a buffer: blocks delivered by belt go straight into the global stock
|
||||||
tr("Building blocks: %1").arg(getContext().sim->getBuildingBlocksStock()));
|
// (REQ-HQ-BELT-INPUT). Showing it here is what tells the player to route them here
|
||||||
|
// (REQ-UI-HQ-PANEL).
|
||||||
|
ItemChipRow::Entry stock;
|
||||||
|
stock.itemId = kBlockItemId;
|
||||||
|
stock.countText = QString::number(getContext().sim->getBuildingBlocksStock());
|
||||||
|
stock.subLine = tr("in stock");
|
||||||
|
m_stockChips->setEntries(std::vector<ItemChipRow::Entry>{ stock });
|
||||||
|
|
||||||
// The HQ's health lives on its proxy entity, not on the building
|
// The HQ's health lives on its proxy entity, not on the building
|
||||||
// (REQ-HQ-STATS, REQ-UI-HP-BARS).
|
// (REQ-HQ-STATS, REQ-UI-HP-BARS).
|
||||||
@@ -35,7 +50,10 @@ void HqContent::refreshRuntime()
|
|||||||
[this](entt::entity /*entity*/, const HqProxyComponent& /*proxy*/,
|
[this](entt::entity /*entity*/, const HqProxyComponent& /*proxy*/,
|
||||||
const HealthComponent& health)
|
const HealthComponent& health)
|
||||||
{
|
{
|
||||||
m_hpLabel->setText(tr("HP: %1 / %2")
|
const double fraction = (health.maxHp > 0.0f)
|
||||||
|
? static_cast<double>(health.hp) / health.maxHp
|
||||||
|
: 0.0;
|
||||||
|
m_hpBar->setValue(fraction, tr("%1 / %2")
|
||||||
.arg(static_cast<int>(health.hp + 0.5f))
|
.arg(static_cast<int>(health.hp + 0.5f))
|
||||||
.arg(static_cast<int>(health.maxHp + 0.5f)));
|
.arg(static_cast<int>(health.maxHp + 0.5f)));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
class QLabel;
|
class BarRow;
|
||||||
|
class ItemChipRow;
|
||||||
|
class SectionBox;
|
||||||
|
|
||||||
// The card for the HQ (REQ-UI-HQ-PANEL): the global building blocks stock and the HQ's
|
// The card for the HQ (REQ-UI-HQ-PANEL): the global building blocks stock and the HQ's
|
||||||
// HP. It has no configuration group and no status indicator.
|
// HP. It has no configuration group and no status indicator.
|
||||||
@@ -24,6 +26,7 @@ protected:
|
|||||||
void refreshRuntime() override;
|
void refreshRuntime() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel* m_stockLabel;
|
SectionBox* m_stockSection;
|
||||||
QLabel* m_hpLabel;
|
ItemChipRow* m_stockChips;
|
||||||
|
BarRow* m_hpBar;
|
||||||
};
|
};
|
||||||
|
|||||||
72
src/ui/selection/ItemChip.cpp
Normal file
72
src/ui/selection/ItemChip.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#include "ItemChip.h"
|
||||||
|
|
||||||
|
#include <QFont>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Point-size rise of the count and drop of the sub-line, relative to the card's text.
|
||||||
|
const int kCountSizeRisePt = 2;
|
||||||
|
const int kSubLineSizeDropPt = 1;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
ItemChip::ItemChip(const QPixmap& icon, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
// Its own boxed chrome, drawn with palette colors like the rest of the panel's
|
||||||
|
// furniture rather than from visuals.toml, which is for world rendering.
|
||||||
|
setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"ItemChip { border: 1px solid palette(mid); border-radius: 3px; }"));
|
||||||
|
|
||||||
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(4, 3, 4, 3);
|
||||||
|
layout->setSpacing(6);
|
||||||
|
|
||||||
|
m_iconLabel = new QLabel(this);
|
||||||
|
m_iconLabel->setPixmap(icon);
|
||||||
|
m_iconLabel->setVisible(!icon.isNull());
|
||||||
|
|
||||||
|
QWidget* text = new QWidget(this);
|
||||||
|
QVBoxLayout* textLayout = new QVBoxLayout(text);
|
||||||
|
textLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
textLayout->setSpacing(0);
|
||||||
|
|
||||||
|
m_countLabel = new QLabel(text);
|
||||||
|
QFont countFont = m_countLabel->font();
|
||||||
|
countFont.setPointSize(countFont.pointSize() + kCountSizeRisePt);
|
||||||
|
m_countLabel->setFont(countFont);
|
||||||
|
|
||||||
|
m_subLineLabel = new QLabel(text);
|
||||||
|
QFont subLineFont = m_subLineLabel->font();
|
||||||
|
subLineFont.setPointSize(qMax(1, subLineFont.pointSize() - kSubLineSizeDropPt));
|
||||||
|
m_subLineLabel->setFont(subLineFont);
|
||||||
|
QPalette subLinePalette = m_subLineLabel->palette();
|
||||||
|
subLinePalette.setColor(QPalette::WindowText,
|
||||||
|
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||||
|
m_subLineLabel->setPalette(subLinePalette);
|
||||||
|
m_subLineLabel->hide();
|
||||||
|
|
||||||
|
textLayout->addWidget(m_countLabel);
|
||||||
|
textLayout->addWidget(m_subLineLabel);
|
||||||
|
|
||||||
|
layout->addWidget(m_iconLabel);
|
||||||
|
layout->addWidget(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemChip::setCount(const QString& count)
|
||||||
|
{
|
||||||
|
m_countLabel->setText(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemChip::setSubLine(const QString& subLine)
|
||||||
|
{
|
||||||
|
m_subLineLabel->setText(subLine);
|
||||||
|
m_subLineLabel->setVisible(!subLine.isEmpty());
|
||||||
|
}
|
||||||
28
src/ui/selection/ItemChip.h
Normal file
28
src/ui/selection/ItemChip.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// One buffered item: its icon, its count in a larger type, and a sub-line beneath the
|
||||||
|
// count (REQ-UI-SINGLE-SELECTION). Boxed so a row of them reads as separate quantities
|
||||||
|
// rather than as a run of text.
|
||||||
|
class ItemChip : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
// An empty icon leaves the icon off and the chip laid out around the text alone.
|
||||||
|
ItemChip(const QPixmap& icon, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setCount(const QString& count);
|
||||||
|
// Left empty for an item with nothing to say beneath its count.
|
||||||
|
void setSubLine(const QString& subLine);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_iconLabel;
|
||||||
|
QLabel* m_countLabel;
|
||||||
|
QLabel* m_subLineLabel;
|
||||||
|
};
|
||||||
78
src/ui/selection/ItemChipRow.cpp
Normal file
78
src/ui/selection/ItemChipRow.cpp
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#include "ItemChipRow.h"
|
||||||
|
|
||||||
|
#include <QGridLayout>
|
||||||
|
|
||||||
|
#include "ItemChip.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Size the item icon is drawn at inside a chip, in device-independent pixels.
|
||||||
|
const int kChipIconSizePx = 18;
|
||||||
|
|
||||||
|
// Chips per row. Two fit the panel's capped width side by side; a third would force the
|
||||||
|
// counts to shrink.
|
||||||
|
const int kChipsPerRow = 2;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
ItemChipRow::ItemChipRow(ItemIconCache* itemIcons, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_itemIcons(itemIcons)
|
||||||
|
{
|
||||||
|
m_layout = new QGridLayout(this);
|
||||||
|
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_layout->setSpacing(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemChipRow::setEntries(const std::vector<Entry>& entries)
|
||||||
|
{
|
||||||
|
std::vector<std::string> itemIds;
|
||||||
|
itemIds.reserve(entries.size());
|
||||||
|
for (const Entry& entry : entries)
|
||||||
|
{
|
||||||
|
itemIds.push_back(entry.itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the set of items changing is a structural change; the counts change every
|
||||||
|
// tick and must not cost a widget rebuild.
|
||||||
|
if (itemIds != m_itemIds)
|
||||||
|
{
|
||||||
|
m_itemIds = std::move(itemIds);
|
||||||
|
rebuildChips(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < entries.size(); ++index)
|
||||||
|
{
|
||||||
|
m_chips[index]->setCount(entries[index].countText);
|
||||||
|
m_chips[index]->setSubLine(entries[index].subLine);
|
||||||
|
}
|
||||||
|
setVisible(!entries.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
|
||||||
|
{
|
||||||
|
for (ItemChip* chip : m_chips)
|
||||||
|
{
|
||||||
|
m_layout->removeWidget(chip);
|
||||||
|
chip->deleteLater();
|
||||||
|
}
|
||||||
|
m_chips.clear();
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < entries.size(); ++index)
|
||||||
|
{
|
||||||
|
const std::string& itemId = entries[index].itemId;
|
||||||
|
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the chip is then laid
|
||||||
|
// out around its count alone.
|
||||||
|
const QPixmap icon = m_itemIcons->hasIcon(itemId)
|
||||||
|
? m_itemIcons->getPixmap(itemId, kChipIconSizePx)
|
||||||
|
: QPixmap();
|
||||||
|
|
||||||
|
ItemChip* chip = new ItemChip(icon, this);
|
||||||
|
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
|
||||||
|
static_cast<int>(index) % kChipsPerRow);
|
||||||
|
m_chips.push_back(chip);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/ui/selection/ItemChipRow.h
Normal file
44
src/ui/selection/ItemChipRow.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class ItemChip;
|
||||||
|
class ItemIconCache;
|
||||||
|
class QGridLayout;
|
||||||
|
|
||||||
|
// The buffered items of one building, each as a chip carrying the item's icon, its
|
||||||
|
// current count and a sub-line (REQ-UI-SINGLE-SELECTION): the per-cycle amount for an
|
||||||
|
// input, the item's name for an output.
|
||||||
|
//
|
||||||
|
// The chips are rebuilt only when the set of items changes, not when their counts do, so
|
||||||
|
// a refresh at tick rate updates numbers instead of churning widgets.
|
||||||
|
class ItemChipRow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct Entry
|
||||||
|
{
|
||||||
|
std::string itemId;
|
||||||
|
QString countText;
|
||||||
|
QString subLine;
|
||||||
|
};
|
||||||
|
|
||||||
|
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
|
||||||
|
// no icon file simply shows no icon, which is not an error. Not owned.
|
||||||
|
explicit ItemChipRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setEntries(const std::vector<Entry>& entries);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuildChips(const std::vector<Entry>& entries);
|
||||||
|
|
||||||
|
ItemIconCache* m_itemIcons;
|
||||||
|
QGridLayout* m_layout;
|
||||||
|
std::vector<std::string> m_itemIds; // what the chips currently stand for
|
||||||
|
std::vector<ItemChip*> m_chips;
|
||||||
|
};
|
||||||
@@ -2,16 +2,26 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "BuildingIconCache.h"
|
||||||
#include "ClearBeltControl.h"
|
#include "ClearBeltControl.h"
|
||||||
|
#include "CountRow.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "SelectionNames.h"
|
#include "SelectionNames.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "StatRow.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Size the type symbol is drawn at on a count row, matching the card header's chip.
|
||||||
|
const int kCountSymbolSizePx = 20;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
|
MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request,
|
const SelectionRequest& request,
|
||||||
@@ -19,10 +29,11 @@ MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
|
|||||||
: SelectionContent(context, std::nullopt, parent)
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
, m_ids(request.buildings)
|
, m_ids(request.buildings)
|
||||||
{
|
{
|
||||||
m_countsLabel = new QLabel(this);
|
// The header names the size of the selection instead of an object
|
||||||
m_totalCostLabel = new QLabel(this);
|
// (REQ-UI-MULTI-SELECTION).
|
||||||
getRuntimeLayout()->addWidget(m_countsLabel);
|
setIdentity(QPixmap(), tr("%1 buildings").arg(static_cast<int>(m_ids.size())));
|
||||||
getRuntimeLayout()->addWidget(m_totalCostLabel);
|
|
||||||
|
buildSummary();
|
||||||
|
|
||||||
// A selection holding any belt-subsystem tile can still be cleared as a whole
|
// A selection holding any belt-subsystem tile can still be cleared as a whole
|
||||||
// (REQ-UI-BELT-CLEAR), even though the mixture is what kept it from aggregating.
|
// (REQ-UI-BELT-CLEAR), even though the mixture is what kept it from aggregating.
|
||||||
@@ -40,12 +51,6 @@ MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
|
|||||||
{
|
{
|
||||||
getRuntimeLayout()->addWidget(new ClearBeltControl(context, m_ids, this));
|
getRuntimeLayout()->addWidget(new ClearBeltControl(context, m_ids, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The header names the size of the selection instead of an object
|
|
||||||
// (REQ-UI-MULTI-SELECTION).
|
|
||||||
setIdentity(QPixmap(), tr("%1 buildings").arg(static_cast<int>(m_ids.size())));
|
|
||||||
|
|
||||||
buildSummary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiBuildingContent::buildSummary()
|
void MultiBuildingContent::buildSummary()
|
||||||
@@ -68,11 +73,13 @@ void MultiBuildingContent::buildSummary()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList lines;
|
|
||||||
int totalCost = 0;
|
int totalCost = 0;
|
||||||
for (const std::pair<const BuildingType, int>& entry : counts)
|
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||||
{
|
{
|
||||||
lines << tr("%1 x %2").arg(getBuildingTypeName(entry.first)).arg(entry.second);
|
getRuntimeLayout()->addWidget(new CountRow(
|
||||||
|
getContext().buildingIcons->getChip(buildingTypeId(entry.first),
|
||||||
|
kCountSymbolSizePx),
|
||||||
|
getBuildingTypeName(entry.first), entry.second, this));
|
||||||
|
|
||||||
// Only player-placeable buildings count toward the total; the HQ and defence
|
// Only player-placeable buildings count toward the total; the HQ and defence
|
||||||
// stations are excluded (REQ-UI-MULTI-SELECTION). A construction site counts at
|
// stations are excluded (REQ-UI-MULTI-SELECTION). A construction site counts at
|
||||||
@@ -85,6 +92,8 @@ void MultiBuildingContent::buildSummary()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_countsLabel->setText(lines.join('\n'));
|
StatRow* totalRow = new StatRow(tr("Total cost"), this);
|
||||||
m_totalCostLabel->setText(tr("Total: %1 Building Blocks").arg(totalCost));
|
totalRow->setValue(QString::number(totalCost));
|
||||||
|
totalRow->setValueEmphasized(true);
|
||||||
|
getRuntimeLayout()->addWidget(totalRow);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
class QLabel;
|
|
||||||
|
|
||||||
// The count summary for several selected buildings that do not aggregate
|
// The count summary for several selected buildings that do not aggregate
|
||||||
// (REQ-UI-MULTI-SELECTION, REQ-UI-SELECTION-AGGREGATE): how many of each type, and the
|
// (REQ-UI-MULTI-SELECTION, REQ-UI-SELECTION-AGGREGATE): how many of each type, and the
|
||||||
@@ -29,6 +28,4 @@ private:
|
|||||||
void buildSummary();
|
void buildSummary();
|
||||||
|
|
||||||
std::vector<BuildingId> m_ids;
|
std::vector<BuildingId> m_ids;
|
||||||
QLabel* m_countsLabel;
|
|
||||||
QLabel* m_totalCostLabel;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BarRow.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "SectionBox.h"
|
||||||
|
|
||||||
ProductionSection::ProductionSection(QWidget* parent)
|
ProductionSection::ProductionSection(QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
@@ -14,40 +15,34 @@ ProductionSection::ProductionSection(QWidget* parent)
|
|||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->setSpacing(0);
|
layout->setSpacing(0);
|
||||||
|
|
||||||
m_label = new QLabel(this);
|
m_section = new SectionBox(tr("Production"), this);
|
||||||
layout->addWidget(m_label);
|
m_bar = new BarRow(QString(), m_section);
|
||||||
|
m_section->getContentLayout()->addWidget(m_bar);
|
||||||
|
layout->addWidget(m_section);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductionSection::setProduction(bool runsProduction, const Building& building,
|
void ProductionSection::setProduction(bool runsProduction, const Building& building,
|
||||||
double durationSeconds, Tick currentTick)
|
double durationSeconds, Tick currentTick)
|
||||||
{
|
{
|
||||||
// Nothing selected to produce means neither a cycle time nor a progress indicator
|
// Nothing selected to produce means no progress indicator at all
|
||||||
// (REQ-UI-PRODUCTION-PROGRESS).
|
// (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
if (!runsProduction)
|
if (!runsProduction)
|
||||||
{
|
{
|
||||||
hide();
|
hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString text;
|
|
||||||
if (durationSeconds > 0.0)
|
|
||||||
{
|
|
||||||
text = tr("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
|
||||||
}
|
|
||||||
if (durationSeconds > 0.0 && building.production.has_value())
|
|
||||||
{
|
|
||||||
const Tick cycleTicks = secondsToTicks(durationSeconds);
|
|
||||||
const Tick elapsed =
|
|
||||||
currentTick - (building.production->completesAt - cycleTicks);
|
|
||||||
const int percent = static_cast<int>(
|
|
||||||
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
|
|
||||||
text += tr("Progress: %1%").arg(percent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
text += tr("Progress: idle");
|
|
||||||
}
|
|
||||||
|
|
||||||
m_label->setText(text);
|
|
||||||
show();
|
show();
|
||||||
|
|
||||||
|
// No running cycle, or none whose length is known: the bar is empty and reads idle.
|
||||||
|
if (durationSeconds <= 0.0 || !building.production.has_value())
|
||||||
|
{
|
||||||
|
m_bar->setValue(0.0, tr("idle"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tick cycleTicks = secondsToTicks(durationSeconds);
|
||||||
|
const Tick elapsed = currentTick - (building.production->completesAt - cycleTicks);
|
||||||
|
const Tick clamped = std::max(Tick(0), std::min(cycleTicks, elapsed));
|
||||||
|
const int percent = static_cast<int>(clamped * 100 / cycleTicks);
|
||||||
|
m_bar->setValue(static_cast<double>(clamped) / cycleTicks, tr("%1%").arg(percent));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
struct Building;
|
struct Building;
|
||||||
class QLabel;
|
class BarRow;
|
||||||
|
class SectionBox;
|
||||||
|
|
||||||
// The cycle time and production progress of one building (REQ-UI-PRODUCTION-PROGRESS).
|
// The cycle time and production progress of one building (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
//
|
//
|
||||||
@@ -29,5 +30,6 @@ public:
|
|||||||
double durationSeconds, Tick currentTick);
|
double durationSeconds, Tick currentTick);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel* m_label;
|
SectionBox* m_section;
|
||||||
|
BarRow* m_bar;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "RecipeSelectionControl.h"
|
#include "RecipeSelectionControl.h"
|
||||||
#include "SelectionNames.h"
|
|
||||||
|
|
||||||
RecipeProductionContent::RecipeProductionContent(const SelectionContext& context,
|
RecipeProductionContent::RecipeProductionContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request,
|
const SelectionRequest& request,
|
||||||
@@ -15,31 +13,25 @@ RecipeProductionContent::RecipeProductionContent(const SelectionContext& context
|
|||||||
{
|
{
|
||||||
const BuildingTarget target = resolveBuildingTarget(context, getBuildingId());
|
const BuildingTarget target = resolveBuildingTarget(context, getBuildingId());
|
||||||
|
|
||||||
// The control is shown for a construction site too: a site is configured exactly
|
// Shown for a construction site too: a site is configured exactly like the building
|
||||||
// like the building it will become (REQ-BLD-SITE-CONFIG).
|
// it will become (REQ-BLD-SITE-CONFIG).
|
||||||
m_recipeControl = new RecipeSelectionControl(context, getBuildingId(), target.type,
|
m_recipeControl = new RecipeSelectionControl(context, getBuildingId(), target.type,
|
||||||
this);
|
this);
|
||||||
getConfigurationLayout()->addWidget(m_recipeControl);
|
getConfigurationLayout()->insertWidget(0, m_recipeControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecipeProductionContent::refreshConfiguration()
|
void RecipeProductionContent::refreshControls(const BuildingTarget& target)
|
||||||
{
|
{
|
||||||
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
|
||||||
if (!target.isValid())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
||||||
m_recipeControl->setRecipeId(target.recipeId);
|
m_recipeControl->setRecipeId(target.recipeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedBuildingContent::CycleInfo RecipeProductionContent::getCycleInfo(
|
BufferedBuildingContent::CycleInfo RecipeProductionContent::getCycleInfo(
|
||||||
const Building& building) const
|
const BuildingTarget& target) const
|
||||||
{
|
{
|
||||||
CycleInfo info;
|
CycleInfo info;
|
||||||
const RecipeDef* recipe = building.recipeId.empty()
|
const RecipeDef* recipe = target.recipeId.empty()
|
||||||
? nullptr
|
? nullptr
|
||||||
: getContext().config->recipes.findRecipeDef(building.recipeId, building.type);
|
: getContext().config->recipes.findRecipeDef(target.recipeId, target.type);
|
||||||
if (!recipe)
|
if (!recipe)
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public:
|
|||||||
const SelectionRequest& request, QWidget* parent = nullptr);
|
const SelectionRequest& request, QWidget* parent = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void refreshConfiguration() override;
|
void refreshControls(const BuildingTarget& target) override;
|
||||||
CycleInfo getCycleInfo(const Building& building) const override;
|
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RecipeSelectionControl* m_recipeControl;
|
RecipeSelectionControl* m_recipeControl;
|
||||||
|
|||||||
108
src/ui/selection/RecipeSummaryRow.cpp
Normal file
108
src/ui/selection/RecipeSummaryRow.cpp
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#include "RecipeSummaryRow.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayoutItem>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
#include "ItemIconCache.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Size the item icons are drawn at on the summary line, in device-independent pixels.
|
||||||
|
const int kSummaryIconSizePx = 14;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
RecipeSummaryRow::RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_itemIcons(itemIcons)
|
||||||
|
{
|
||||||
|
m_layout = new QHBoxLayout(this);
|
||||||
|
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_layout->setSpacing(4);
|
||||||
|
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecipeSummaryRow::setSummary(const std::vector<Amount>& inputs,
|
||||||
|
const std::vector<Amount>& outputs,
|
||||||
|
double durationSeconds)
|
||||||
|
{
|
||||||
|
if (outputs.empty() && inputs.empty())
|
||||||
|
{
|
||||||
|
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
|
||||||
|
// a change and shows the row again.
|
||||||
|
m_inputs.clear();
|
||||||
|
m_outputs.clear();
|
||||||
|
m_durationSeconds = -1.0;
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inputs == m_inputs && outputs == m_outputs
|
||||||
|
&& durationSeconds == m_durationSeconds)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inputs = inputs;
|
||||||
|
m_outputs = outputs;
|
||||||
|
m_durationSeconds = durationSeconds;
|
||||||
|
rebuild(inputs, outputs, durationSeconds);
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecipeSummaryRow::rebuild(const std::vector<Amount>& inputs,
|
||||||
|
const std::vector<Amount>& outputs,
|
||||||
|
double durationSeconds)
|
||||||
|
{
|
||||||
|
while (QLayoutItem* item = m_layout->takeAt(0))
|
||||||
|
{
|
||||||
|
if (item->widget())
|
||||||
|
{
|
||||||
|
item->widget()->deleteLater();
|
||||||
|
}
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAmounts(inputs);
|
||||||
|
if (!inputs.empty())
|
||||||
|
{
|
||||||
|
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
||||||
|
m_layout->addWidget(new QLabel(QString(rightArrow), this));
|
||||||
|
}
|
||||||
|
addAmounts(outputs);
|
||||||
|
|
||||||
|
if (durationSeconds > 0.0)
|
||||||
|
{
|
||||||
|
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
||||||
|
m_layout->addWidget(new QLabel(
|
||||||
|
QStringLiteral("%1 %2").arg(middleDot).arg(
|
||||||
|
tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this));
|
||||||
|
}
|
||||||
|
m_layout->addStretch(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
|
||||||
|
{
|
||||||
|
for (const Amount& entry : amounts)
|
||||||
|
{
|
||||||
|
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the item's id then
|
||||||
|
// stands in for its icon.
|
||||||
|
if (m_itemIcons->hasIcon(entry.itemId))
|
||||||
|
{
|
||||||
|
QLabel* iconLabel = new QLabel(this);
|
||||||
|
iconLabel->setPixmap(
|
||||||
|
m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx));
|
||||||
|
m_layout->addWidget(iconLabel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_layout->addWidget(
|
||||||
|
new QLabel(QString::fromStdString(entry.itemId), this));
|
||||||
|
}
|
||||||
|
m_layout->addWidget(new QLabel(QString::number(entry.amount), this));
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/ui/selection/RecipeSummaryRow.h
Normal file
52
src/ui/selection/RecipeSummaryRow.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class ItemIconCache;
|
||||||
|
class QHBoxLayout;
|
||||||
|
|
||||||
|
// What one production cycle does, on a line: each input item with its per-cycle amount,
|
||||||
|
// an arrow, each output with its amount, and the cycle time
|
||||||
|
// (REQ-UI-RECIPE-SUMMARY). It restates the selected recipe without the player having to
|
||||||
|
// open the selection dialog, and it is the panel's only display of the cycle time.
|
||||||
|
class RecipeSummaryRow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct Amount
|
||||||
|
{
|
||||||
|
std::string itemId;
|
||||||
|
int amount = 0;
|
||||||
|
|
||||||
|
bool operator==(const Amount& other) const
|
||||||
|
{
|
||||||
|
return itemId == other.itemId && amount == other.amount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
|
||||||
|
// no icon file falls back to its id. Not owned.
|
||||||
|
explicit RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// Hides the row when there is nothing selected to produce, which is how a building
|
||||||
|
// with no recipe shows no summary at all.
|
||||||
|
void setSummary(const std::vector<Amount>& inputs,
|
||||||
|
const std::vector<Amount>& outputs, double durationSeconds);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuild(const std::vector<Amount>& inputs, const std::vector<Amount>& outputs,
|
||||||
|
double durationSeconds);
|
||||||
|
void addAmounts(const std::vector<Amount>& amounts);
|
||||||
|
|
||||||
|
ItemIconCache* m_itemIcons;
|
||||||
|
QHBoxLayout* m_layout;
|
||||||
|
// What the row currently shows, so a refresh at tick rate rebuilds it only when the
|
||||||
|
// recipe actually changed.
|
||||||
|
std::vector<Amount> m_inputs;
|
||||||
|
std::vector<Amount> m_outputs;
|
||||||
|
double m_durationSeconds = -1.0;
|
||||||
|
};
|
||||||
50
src/ui/selection/SectionBox.cpp
Normal file
50
src/ui/selection/SectionBox.cpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#include "SectionBox.h"
|
||||||
|
|
||||||
|
#include <QFont>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Point-size drop of the caption relative to the card's text, so it reads as a heading
|
||||||
|
// without competing with the values beneath it.
|
||||||
|
const int kCaptionSizeDropPt = 1;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
SectionBox::SectionBox(const QString& caption, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(2);
|
||||||
|
|
||||||
|
m_captionLabel = new QLabel(caption.toUpper(), this);
|
||||||
|
QFont captionFont = m_captionLabel->font();
|
||||||
|
captionFont.setPointSize(qMax(1, captionFont.pointSize() - kCaptionSizeDropPt));
|
||||||
|
captionFont.setBold(true);
|
||||||
|
m_captionLabel->setFont(captionFont);
|
||||||
|
|
||||||
|
// Dimmed to the palette's disabled text, so the caption sits behind its content.
|
||||||
|
QPalette captionPalette = m_captionLabel->palette();
|
||||||
|
captionPalette.setColor(QPalette::WindowText,
|
||||||
|
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||||
|
m_captionLabel->setPalette(captionPalette);
|
||||||
|
m_captionLabel->setVisible(!caption.isEmpty());
|
||||||
|
|
||||||
|
QWidget* content = new QWidget(this);
|
||||||
|
m_contentLayout = new QVBoxLayout(content);
|
||||||
|
m_contentLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_contentLayout->setSpacing(2);
|
||||||
|
|
||||||
|
layout->addWidget(m_captionLabel);
|
||||||
|
layout->addWidget(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVBoxLayout* SectionBox::getContentLayout()
|
||||||
|
{
|
||||||
|
return m_contentLayout;
|
||||||
|
}
|
||||||
28
src/ui/selection/SectionBox.h
Normal file
28
src/ui/selection/SectionBox.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// A captioned group of parts within a card's configuration or runtime group
|
||||||
|
// (REQ-UI-SELECTION-CARD): a small heading over whatever the owner puts inside it.
|
||||||
|
//
|
||||||
|
// A section and its caption are shown or hidden as one, which is what lets a card list
|
||||||
|
// its sections unconditionally and simply hide the ones with nothing in them -- a Miner
|
||||||
|
// consumes nothing, so its input buffer section is never shown.
|
||||||
|
class SectionBox : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SectionBox(const QString& caption, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// The layout to add this section's parts to.
|
||||||
|
QVBoxLayout* getContentLayout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_captionLabel;
|
||||||
|
QVBoxLayout* m_contentLayout;
|
||||||
|
};
|
||||||
@@ -4,18 +4,19 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QGuiApplication>
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPainter>
|
|
||||||
#include <QRectF>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BarRow.h"
|
||||||
#include "BuildingIconCache.h"
|
#include "BuildingIconCache.h"
|
||||||
|
#include "EmptyNote.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
|
#include "SectionBox.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "StatusPill.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
@@ -27,31 +28,10 @@ namespace
|
|||||||
// the whole button face.
|
// the whole button face.
|
||||||
const int kSymbolSizePx = 20;
|
const int kSymbolSizePx = 20;
|
||||||
|
|
||||||
// Diameter of the status dot beside the header's right-slot caption
|
|
||||||
// (REQ-UI-SELECTION-STATUS).
|
|
||||||
const int kStatusDotSizePx = 8;
|
|
||||||
|
|
||||||
// Spacing inside the card and between the header's elements.
|
// Spacing inside the card and between the header's elements.
|
||||||
const int kCardSpacingPx = 6;
|
const int kCardSpacingPx = 6;
|
||||||
const int kHeaderSpacingPx = 6;
|
const int kHeaderSpacingPx = 6;
|
||||||
|
|
||||||
QPixmap renderStatusDot(const QColor& fill, const QColor& outline)
|
|
||||||
{
|
|
||||||
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
||||||
QPixmap pixmap(static_cast<int>(kStatusDotSizePx * dpr),
|
|
||||||
static_cast<int>(kStatusDotSizePx * dpr));
|
|
||||||
pixmap.setDevicePixelRatio(dpr);
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
painter.setPen(outline);
|
|
||||||
painter.setBrush(fill);
|
|
||||||
// Inset by half the pen width so the outline stays inside the pixmap.
|
|
||||||
painter.drawEllipse(QRectF(0.5, 0.5, kStatusDotSizePx - 1.0, kStatusDotSizePx - 1.0));
|
|
||||||
return pixmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -61,7 +41,8 @@ SelectionContent::SelectionContent(const SelectionContext& context,
|
|||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_context(context)
|
, m_context(context)
|
||||||
, m_siteId(constructionSiteId)
|
, m_siteId(constructionSiteId)
|
||||||
, m_constructionLabel(nullptr)
|
, m_constructionSection(nullptr)
|
||||||
|
, m_constructionBar(nullptr)
|
||||||
{
|
{
|
||||||
QVBoxLayout* cardLayout = new QVBoxLayout(this);
|
QVBoxLayout* cardLayout = new QVBoxLayout(this);
|
||||||
cardLayout->setContentsMargins(0, 0, 0, 0);
|
cardLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
@@ -82,17 +63,12 @@ SelectionContent::SelectionContent(const SelectionContext& context,
|
|||||||
nameFont.setBold(true);
|
nameFont.setBold(true);
|
||||||
m_nameLabel->setFont(nameFont);
|
m_nameLabel->setFont(nameFont);
|
||||||
|
|
||||||
m_slotDot = new QLabel(header);
|
m_statusPill = new StatusPill(header);
|
||||||
m_slotDot->hide();
|
|
||||||
|
|
||||||
m_slotLabel = new QLabel(header);
|
|
||||||
m_slotLabel->hide();
|
|
||||||
|
|
||||||
headerLayout->addWidget(m_symbolLabel);
|
headerLayout->addWidget(m_symbolLabel);
|
||||||
headerLayout->addWidget(m_nameLabel);
|
headerLayout->addWidget(m_nameLabel);
|
||||||
headerLayout->addStretch(1);
|
headerLayout->addStretch(1);
|
||||||
headerLayout->addWidget(m_slotDot);
|
headerLayout->addWidget(m_statusPill);
|
||||||
headerLayout->addWidget(m_slotLabel);
|
|
||||||
cardLayout->addWidget(header);
|
cardLayout->addWidget(header);
|
||||||
|
|
||||||
m_configurationGroup = new QWidget(this);
|
m_configurationGroup = new QWidget(this);
|
||||||
@@ -114,8 +90,13 @@ SelectionContent::SelectionContent(const SelectionContext& context,
|
|||||||
// takes its place (REQ-BLD-SITE-CONFIG, REQ-UI-SELECTION-CARD). The subclass
|
// takes its place (REQ-BLD-SITE-CONFIG, REQ-UI-SELECTION-CARD). The subclass
|
||||||
// still fills the group -- it just never becomes visible.
|
// still fills the group -- it just never becomes visible.
|
||||||
m_runtimeGroup->hide();
|
m_runtimeGroup->hide();
|
||||||
m_constructionLabel = new QLabel(this);
|
|
||||||
cardLayout->addWidget(m_constructionLabel);
|
m_constructionSection = new SectionBox(tr("Construction"), this);
|
||||||
|
m_constructionBar = new BarRow(QString(), m_constructionSection);
|
||||||
|
m_constructionSection->getContentLayout()->addWidget(m_constructionBar);
|
||||||
|
m_constructionSection->getContentLayout()->addWidget(
|
||||||
|
new EmptyNote(tr("No buffers until built"), m_constructionSection));
|
||||||
|
cardLayout->addWidget(m_constructionSection);
|
||||||
|
|
||||||
setSlot(QColor(), tr("constructing"));
|
setSlot(QColor(), tr("constructing"));
|
||||||
}
|
}
|
||||||
@@ -159,18 +140,7 @@ void SelectionContent::setBuildingIdentity(BuildingType type, const QString& nam
|
|||||||
|
|
||||||
void SelectionContent::setSlot(const QColor& dotColor, const QString& caption)
|
void SelectionContent::setSlot(const QColor& dotColor, const QString& caption)
|
||||||
{
|
{
|
||||||
if (dotColor.isValid())
|
m_statusPill->setStatus(dotColor, m_context.visuals->statusLight.outline, caption);
|
||||||
{
|
|
||||||
m_slotDot->setPixmap(
|
|
||||||
renderStatusDot(dotColor, m_context.visuals->statusLight.outline));
|
|
||||||
m_slotDot->show();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_slotDot->hide();
|
|
||||||
}
|
|
||||||
m_slotLabel->setText(caption);
|
|
||||||
m_slotLabel->setVisible(!caption.isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionContent::setCountSlot(int count)
|
void SelectionContent::setCountSlot(int count)
|
||||||
@@ -227,28 +197,26 @@ void SelectionContent::refreshConstruction()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString progress;
|
|
||||||
if (site->completesAt == 0)
|
if (site->completesAt == 0)
|
||||||
{
|
{
|
||||||
progress = tr("Queued");
|
// Placed but not yet at the head of the construction queue (REQ-BLD-QUEUE), so
|
||||||
|
// there is no progress to show yet.
|
||||||
|
m_constructionBar->setValue(0.0, tr("Queued"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
const BuildingDef* def = m_context.config->buildings.findBuildingDef(site->type);
|
||||||
|
if (!def || def->constructionTimeSeconds <= 0)
|
||||||
{
|
{
|
||||||
const BuildingDef* def =
|
m_constructionBar->setValue(0.0, tr("Building..."));
|
||||||
m_context.config->buildings.findBuildingDef(site->type);
|
return;
|
||||||
if (def && def->constructionTimeSeconds > 0)
|
|
||||||
{
|
|
||||||
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
|
||||||
const Tick elapsed =
|
|
||||||
m_context.sim->getCurrentTick() - (site->completesAt - duration);
|
|
||||||
const int percent = static_cast<int>(
|
|
||||||
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
|
|
||||||
progress = tr("Construction: %1%").arg(percent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
progress = tr("Building...");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m_constructionLabel->setText(progress);
|
|
||||||
|
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
||||||
|
const Tick elapsed =
|
||||||
|
m_context.sim->getCurrentTick() - (site->completesAt - duration);
|
||||||
|
const Tick clamped = std::max(Tick(0), std::min(duration, elapsed));
|
||||||
|
const int percent = static_cast<int>(clamped * 100 / duration);
|
||||||
|
m_constructionBar->setValue(static_cast<double>(clamped) / duration,
|
||||||
|
tr("%1%").arg(percent));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,11 @@
|
|||||||
#include "SelectionContext.h"
|
#include "SelectionContext.h"
|
||||||
|
|
||||||
struct Building;
|
struct Building;
|
||||||
|
class BarRow;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
class SectionBox;
|
||||||
|
class StatusPill;
|
||||||
|
|
||||||
// One card of the selection panel: the content shown for a particular kind of selection
|
// One card of the selection panel: the content shown for a particular kind of selection
|
||||||
// (REQ-UI-SELECTION-CARD). Every content is this base plus the parts its constructor
|
// (REQ-UI-SELECTION-CARD). Every content is this base plus the parts its constructor
|
||||||
@@ -88,14 +91,14 @@ private:
|
|||||||
// Set while this card shows a construction site rather than a finished object.
|
// Set while this card shows a construction site rather than a finished object.
|
||||||
std::optional<BuildingId> m_siteId;
|
std::optional<BuildingId> m_siteId;
|
||||||
|
|
||||||
QLabel* m_symbolLabel;
|
QLabel* m_symbolLabel;
|
||||||
QLabel* m_nameLabel;
|
QLabel* m_nameLabel;
|
||||||
QLabel* m_slotDot;
|
StatusPill* m_statusPill;
|
||||||
QLabel* m_slotLabel;
|
|
||||||
|
|
||||||
QWidget* m_configurationGroup;
|
QWidget* m_configurationGroup;
|
||||||
QWidget* m_runtimeGroup;
|
QWidget* m_runtimeGroup;
|
||||||
// Replaces the runtime group while this card shows a construction site; null
|
// Replaces the runtime group while this card shows a construction site; both null
|
||||||
// otherwise.
|
// otherwise.
|
||||||
QLabel* m_constructionLabel;
|
SectionBox* m_constructionSection;
|
||||||
|
BarRow* m_constructionBar;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,3 +14,22 @@ QString getBuildingTypeName(BuildingType type)
|
|||||||
}
|
}
|
||||||
return QString::fromStdString(toDisplayName(buildingTypeId(type)));
|
return QString::fromStdString(toDisplayName(buildingTypeId(type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString getBehaviorLabel(BehaviorKind kind)
|
||||||
|
{
|
||||||
|
// Only the winning behavior is named; the salvage and repair cycles that run
|
||||||
|
// regardless of it are not behaviors here (REQ-UI-SHIP-BEHAVIOR).
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case BehaviorKind::Retreat: return QObject::tr("Retreating");
|
||||||
|
case BehaviorKind::Attack: return QObject::tr("Engaging");
|
||||||
|
case BehaviorKind::SalvageScrap:
|
||||||
|
case BehaviorKind::DeliverScrap: return QObject::tr("Salvaging");
|
||||||
|
case BehaviorKind::Repair: return QObject::tr("Repairing");
|
||||||
|
case BehaviorKind::Rally: return QObject::tr("Rallying");
|
||||||
|
case BehaviorKind::Standby: return QObject::tr("Standby");
|
||||||
|
case BehaviorKind::Advance: return QObject::tr("Advancing");
|
||||||
|
case BehaviorKind::None: break;
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include "BehaviorKind.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
|
||||||
// Display name of a building type for the selection panel's header and count rows
|
// Display name of a building type for the selection panel's header and count rows
|
||||||
// (REQ-UI-SELECTION-CARD, REQ-UI-MULTI-SELECTION). The name is derived from the type's
|
// (REQ-UI-SELECTION-CARD, REQ-UI-MULTI-SELECTION). The name is derived from the type's
|
||||||
// config id, so a new building type needs no entry here.
|
// config id, so a new building type needs no entry here.
|
||||||
QString getBuildingTypeName(BuildingType type);
|
QString getBuildingTypeName(BuildingType type);
|
||||||
|
|
||||||
|
// Name of the behavior currently governing a ship, for the ship card's header slot
|
||||||
|
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
|
||||||
|
QString getBehaviorLabel(BehaviorKind kind);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "SelectedBehaviorComponent.h"
|
#include "SelectedBehaviorComponent.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
#include "ShipStatsPanel.h"
|
#include "ShipStatsPanel.h"
|
||||||
@@ -47,9 +48,13 @@ void ShipContent::refreshRuntime()
|
|||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, m_entity);
|
const ShipStats stats = buildShipStatsFromEntity(admin, m_entity);
|
||||||
m_statsPanel->refreshFromLive(stats, health.hp);
|
m_statsPanel->refreshFromLive(stats, health.hp);
|
||||||
m_statsPanel->setBehavior(admin.get<SelectedBehaviorComponent>(m_entity).winner);
|
|
||||||
m_statsPanel->setDebugDrawEnabled(*getContext().debugDrawEnabled);
|
m_statsPanel->setDebugDrawEnabled(*getContext().debugDrawEnabled);
|
||||||
|
|
||||||
|
// The behavior is the header's right slot rather than a stat row, so it reads as
|
||||||
|
// what the ship is doing rather than as another number (REQ-UI-SHIP-BEHAVIOR).
|
||||||
|
setSlot(QColor(), getBehaviorLabel(
|
||||||
|
admin.get<SelectedBehaviorComponent>(m_entity).winner));
|
||||||
|
|
||||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(m_entity);
|
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(m_entity);
|
||||||
const ShipDef* schematicDef =
|
const ShipDef* schematicDef =
|
||||||
getContext().config->ships.findShipDef(identity.schematicId);
|
getContext().config->ships.findShipDef(identity.schematicId);
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "LayoutDialogRequestedEvent.h"
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "RecipeSelectionControl.h"
|
#include "RecipeSelectionControl.h"
|
||||||
#include "SelectionNames.h"
|
#include "SectionBox.h"
|
||||||
#include "ShipLayoutPreview.h"
|
#include "ShipLayoutPreview.h"
|
||||||
|
|
||||||
ShipyardContent::ShipyardContent(const SelectionContext& context,
|
ShipyardContent::ShipyardContent(const SelectionContext& context,
|
||||||
@@ -19,12 +18,17 @@ ShipyardContent::ShipyardContent(const SelectionContext& context,
|
|||||||
{
|
{
|
||||||
m_schematicControl = new RecipeSelectionControl(context, getBuildingId(),
|
m_schematicControl = new RecipeSelectionControl(context, getBuildingId(),
|
||||||
BuildingType::Shipyard, this);
|
BuildingType::Shipyard, this);
|
||||||
m_layoutPreview = new ShipLayoutPreview(this);
|
|
||||||
m_configureButton = new QPushButton(tr("Configure Layout"), this);
|
|
||||||
|
|
||||||
getConfigurationLayout()->addWidget(m_schematicControl);
|
m_layoutSection = new SectionBox(tr("Layout"), this);
|
||||||
getConfigurationLayout()->addWidget(m_layoutPreview);
|
m_layoutPreview = new ShipLayoutPreview(m_layoutSection);
|
||||||
getConfigurationLayout()->addWidget(m_configureButton);
|
m_configureButton = new QPushButton(tr("Configure Layout"), m_layoutSection);
|
||||||
|
m_layoutSection->getContentLayout()->addWidget(m_layoutPreview);
|
||||||
|
m_layoutSection->getContentLayout()->addWidget(m_configureButton);
|
||||||
|
|
||||||
|
// Ahead of the recipe summary the base adds, so the card reads schematic, layout,
|
||||||
|
// then what one ship costs.
|
||||||
|
getConfigurationLayout()->insertWidget(0, m_schematicControl);
|
||||||
|
getConfigurationLayout()->insertWidget(1, m_layoutSection);
|
||||||
|
|
||||||
const BuildingId id = getBuildingId();
|
const BuildingId id = getBuildingId();
|
||||||
connect(m_configureButton, &QPushButton::clicked, this, [id]() {
|
connect(m_configureButton, &QPushButton::clicked, this, [id]() {
|
||||||
@@ -33,15 +37,8 @@ ShipyardContent::ShipyardContent(const SelectionContext& context,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipyardContent::refreshConfiguration()
|
void ShipyardContent::refreshControls(const BuildingTarget& target)
|
||||||
{
|
{
|
||||||
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
|
||||||
if (!target.isValid())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
||||||
m_schematicControl->setRecipeId(target.recipeId);
|
m_schematicControl->setRecipeId(target.recipeId);
|
||||||
|
|
||||||
// The preview and Configure button are always shown for a shipyard and are only
|
// The preview and Configure button are always shown for a shipyard and are only
|
||||||
@@ -68,12 +65,12 @@ void ShipyardContent::refreshConfiguration()
|
|||||||
}
|
}
|
||||||
|
|
||||||
BufferedBuildingContent::CycleInfo ShipyardContent::getCycleInfo(
|
BufferedBuildingContent::CycleInfo ShipyardContent::getCycleInfo(
|
||||||
const Building& building) const
|
const BuildingTarget& target) const
|
||||||
{
|
{
|
||||||
CycleInfo info;
|
CycleInfo info;
|
||||||
const ShipDef* shipDef = building.recipeId.empty()
|
const ShipDef* shipDef = target.recipeId.empty()
|
||||||
? nullptr
|
? nullptr
|
||||||
: getContext().config->ships.findShipDef(building.recipeId);
|
: getContext().config->ships.findShipDef(target.recipeId);
|
||||||
if (!shipDef)
|
if (!shipDef)
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
@@ -82,13 +79,15 @@ BufferedBuildingContent::CycleInfo ShipyardContent::getCycleInfo(
|
|||||||
// The schematic's materials plus every placed module's, which is also what sized the
|
// The schematic's materials plus every placed module's, which is also what sized the
|
||||||
// input buffers (REQ-BLD-SHIPYARD). The simulation owns that sum, so the panel asks
|
// input buffers (REQ-BLD-SHIPYARD). The simulation owns that sum, so the panel asks
|
||||||
// it rather than adding the modules up a second time.
|
// it rather than adding the modules up a second time.
|
||||||
info.perCycleInputs =
|
info.perCycleInputs = computeShipyardRequiredMaterials(
|
||||||
computeShipyardRequiredMaterials(*getContext().config, building);
|
*getContext().config, target.recipeId, target.shipLayout);
|
||||||
|
|
||||||
|
// A shipyard's output is the ship itself, which never lands in an item buffer -- so
|
||||||
|
// the summary shows one ship rather than an item id.
|
||||||
info.durationSeconds = shipDef->schematic.productionTimeSeconds;
|
info.durationSeconds = shipDef->schematic.productionTimeSeconds;
|
||||||
if (building.shipLayout.has_value())
|
if (target.shipLayout.has_value())
|
||||||
{
|
{
|
||||||
for (const PlacedModule& placed : building.shipLayout->placedModules)
|
for (const PlacedModule& placed : target.shipLayout->placedModules)
|
||||||
{
|
{
|
||||||
const ModuleDef* moduleDef =
|
const ModuleDef* moduleDef =
|
||||||
getContext().config->modules.findModuleDef(placed.moduleId);
|
getContext().config->modules.findModuleDef(placed.moduleId);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class RecipeSelectionControl;
|
class RecipeSelectionControl;
|
||||||
|
class SectionBox;
|
||||||
class ShipLayoutPreview;
|
class ShipLayoutPreview;
|
||||||
|
|
||||||
// The card for a Shipyard (REQ-UI-SELECTION-CONTENT): the schematic selection, the
|
// The card for a Shipyard (REQ-UI-SELECTION-CONTENT): the schematic selection, the
|
||||||
@@ -22,11 +23,12 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void refreshConfiguration() override;
|
void refreshControls(const BuildingTarget& target) override;
|
||||||
CycleInfo getCycleInfo(const Building& building) const override;
|
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RecipeSelectionControl* m_schematicControl;
|
RecipeSelectionControl* m_schematicControl;
|
||||||
|
SectionBox* m_layoutSection;
|
||||||
ShipLayoutPreview* m_layoutPreview;
|
ShipLayoutPreview* m_layoutPreview;
|
||||||
QPushButton* m_configureButton;
|
QPushButton* m_configureButton;
|
||||||
};
|
};
|
||||||
|
|||||||
54
src/ui/selection/StatRow.cpp
Normal file
54
src/ui/selection/StatRow.cpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#include "StatRow.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPalette>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Left inset of an indented row, in device-independent pixels.
|
||||||
|
const int kIndentPx = 12;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
StatRow::StatRow(const QString& label, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(8);
|
||||||
|
|
||||||
|
m_labelLabel = new QLabel(label, this);
|
||||||
|
m_valueLabel = new QLabel(this);
|
||||||
|
m_valueLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
|
||||||
|
layout->addWidget(m_labelLabel);
|
||||||
|
layout->addStretch(1);
|
||||||
|
layout->addWidget(m_valueLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatRow::setLabel(const QString& label)
|
||||||
|
{
|
||||||
|
m_labelLabel->setText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatRow::setValue(const QString& value)
|
||||||
|
{
|
||||||
|
m_valueLabel->setText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatRow::setValueEmphasized(bool emphasized)
|
||||||
|
{
|
||||||
|
QPalette valuePalette = m_valueLabel->palette();
|
||||||
|
valuePalette.setColor(QPalette::WindowText,
|
||||||
|
palette().color(emphasized ? QPalette::Highlight
|
||||||
|
: QPalette::WindowText));
|
||||||
|
m_valueLabel->setPalette(valuePalette);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatRow::setIndented(bool indented)
|
||||||
|
{
|
||||||
|
layout()->setContentsMargins(indented ? kIndentPx : 0, 0, 0, 0);
|
||||||
|
}
|
||||||
33
src/ui/selection/StatRow.h
Normal file
33
src/ui/selection/StatRow.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// One label/value line: the name on the left, the value hard right
|
||||||
|
// (REQ-UI-SELECTION-CARD). The panel's plainest part, shared by the ship and station
|
||||||
|
// stats, the debris scrap, the HQ block stock, and the multi-selection's total cost.
|
||||||
|
//
|
||||||
|
// Like the other card parts it knows nothing about the simulation: it is given the two
|
||||||
|
// strings and lays them out.
|
||||||
|
class StatRow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit StatRow(const QString& label, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setLabel(const QString& label);
|
||||||
|
void setValue(const QString& value);
|
||||||
|
// Draws the value in the palette's highlight color rather than its text color, for
|
||||||
|
// the one value a card is really about.
|
||||||
|
void setValueEmphasized(bool emphasized);
|
||||||
|
// Indents the row, so it reads as belonging to the row above it -- the scrap total
|
||||||
|
// under a debris count (REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
void setIndented(bool indented);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_labelLabel;
|
||||||
|
QLabel* m_valueLabel;
|
||||||
|
};
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
#include "StationContent.h"
|
#include "StationContent.h"
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BarRow.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "StatRow.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
StationContent::StationContent(const SelectionContext& context,
|
StationContent::StationContent(const SelectionContext& context,
|
||||||
@@ -15,9 +16,15 @@ StationContent::StationContent(const SelectionContext& context,
|
|||||||
: SelectionContent(context, std::nullopt, parent)
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
, m_entity(request.actors.front())
|
, m_entity(request.actors.front())
|
||||||
{
|
{
|
||||||
m_statsLabel = new QLabel(this);
|
m_hpBar = new BarRow(tr("HP"), this);
|
||||||
m_statsLabel->setWordWrap(true);
|
m_damageRow = new StatRow(tr("Damage"), this);
|
||||||
getRuntimeLayout()->addWidget(m_statsLabel);
|
m_rangeRow = new StatRow(tr("Range"), this);
|
||||||
|
m_fireRateRow = new StatRow(tr("Fire rate"), this);
|
||||||
|
|
||||||
|
getRuntimeLayout()->addWidget(m_hpBar);
|
||||||
|
getRuntimeLayout()->addWidget(m_damageRow);
|
||||||
|
getRuntimeLayout()->addWidget(m_rangeRow);
|
||||||
|
getRuntimeLayout()->addWidget(m_fireRateRow);
|
||||||
|
|
||||||
EntityAdmin& admin = context.sim->getAdmin();
|
EntityAdmin& admin = context.sim->getAdmin();
|
||||||
const bool isEnemy = admin.isValid(m_entity)
|
const bool isEnemy = admin.isValid(m_entity)
|
||||||
@@ -36,11 +43,19 @@ void StationContent::refreshRuntime()
|
|||||||
}
|
}
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(m_entity);
|
const HealthComponent& health = admin.get<HealthComponent>(m_entity);
|
||||||
|
|
||||||
// A station's weapons are child module entities pointing back at it, so its combined
|
const double hpFraction = (health.maxHp > 0.0f)
|
||||||
// damage and range are summed over those rather than read off the station itself.
|
? static_cast<double>(health.hp) / health.maxHp
|
||||||
float totalDps = 0.0f;
|
: 0.0;
|
||||||
float maxRange = 0.0f;
|
m_hpBar->setValue(hpFraction, tr("%1 / %2")
|
||||||
bool hasWeapon = false;
|
.arg(static_cast<int>(health.hp + 0.5f))
|
||||||
|
.arg(static_cast<int>(health.maxHp + 0.5f)));
|
||||||
|
|
||||||
|
// A station's weapons are child module entities pointing back at it, so its damage,
|
||||||
|
// range and fire rate are read off those rather than off the station itself.
|
||||||
|
float totalDamage = 0.0f;
|
||||||
|
float maxRange = 0.0f;
|
||||||
|
float maxFireRateHz = 0.0f;
|
||||||
|
bool hasWeapon = false;
|
||||||
const entt::entity station = m_entity;
|
const entt::entity station = m_entity;
|
||||||
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
||||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner,
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner,
|
||||||
@@ -48,19 +63,21 @@ void StationContent::refreshRuntime()
|
|||||||
{
|
{
|
||||||
if (owner.owner != station) { return; }
|
if (owner.owner != station) { return; }
|
||||||
hasWeapon = true;
|
hasWeapon = true;
|
||||||
totalDps += weapon.damage * weapon.fireRateHz;
|
totalDamage += weapon.damage;
|
||||||
if (weapon.range_tiles > maxRange) { maxRange = weapon.range_tiles; }
|
if (weapon.range_tiles > maxRange) { maxRange = weapon.range_tiles; }
|
||||||
|
if (weapon.fireRateHz > maxFireRateHz) { maxFireRateHz = weapon.fireRateHz; }
|
||||||
});
|
});
|
||||||
|
|
||||||
QString text = tr("HP: %1 / %2")
|
m_damageRow->setVisible(hasWeapon);
|
||||||
.arg(static_cast<int>(health.hp + 0.5f))
|
m_rangeRow->setVisible(hasWeapon);
|
||||||
.arg(static_cast<int>(health.maxHp + 0.5f));
|
m_fireRateRow->setVisible(hasWeapon);
|
||||||
if (hasWeapon)
|
if (hasWeapon)
|
||||||
{
|
{
|
||||||
text += tr("\nDPS: %1")
|
m_damageRow->setValue(
|
||||||
.arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
QString::number(static_cast<double>(totalDamage), 'f', 1));
|
||||||
text += tr("\nRange: %1 tiles")
|
m_rangeRow->setValue(tr("%1 tiles")
|
||||||
.arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
.arg(QString::number(static_cast<double>(maxRange), 'f', 1)));
|
||||||
|
m_fireRateRow->setValue(tr("%1 /s")
|
||||||
|
.arg(QString::number(static_cast<double>(maxFireRateHz), 'f', 1)));
|
||||||
}
|
}
|
||||||
m_statsLabel->setText(text);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
#include "SelectionContent.h"
|
#include "SelectionContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
class QLabel;
|
class BarRow;
|
||||||
|
class StatRow;
|
||||||
|
|
||||||
// The card for one selected defence station, player or enemy
|
// The card for one selected defence station, player or enemy
|
||||||
// (REQ-UI-STATION-STATS-PANEL): its HP plus the combined damage, range and fire rate of
|
// (REQ-UI-STATION-STATS-PANEL): its HP plus the combined damage, range and fire rate of
|
||||||
@@ -23,5 +24,8 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
entt::entity m_entity;
|
entt::entity m_entity;
|
||||||
QLabel* m_statsLabel;
|
BarRow* m_hpBar;
|
||||||
|
StatRow* m_damageRow;
|
||||||
|
StatRow* m_rangeRow;
|
||||||
|
StatRow* m_fireRateRow;
|
||||||
};
|
};
|
||||||
|
|||||||
68
src/ui/selection/StatusPill.cpp
Normal file
68
src/ui/selection/StatusPill.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#include "StatusPill.h"
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QRectF>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Diameter of the status dot, in device-independent pixels.
|
||||||
|
const int kDotSizePx = 8;
|
||||||
|
|
||||||
|
QPixmap renderDot(const QColor& fill, const QColor& outline)
|
||||||
|
{
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QPixmap pixmap(static_cast<int>(kDotSizePx * dpr),
|
||||||
|
static_cast<int>(kDotSizePx * dpr));
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setPen(outline.isValid() ? QPen(outline) : QPen(Qt::NoPen));
|
||||||
|
painter.setBrush(fill);
|
||||||
|
// Inset by half the pen width so the outline stays inside the pixmap.
|
||||||
|
painter.drawEllipse(QRectF(0.5, 0.5, kDotSizePx - 1.0, kDotSizePx - 1.0));
|
||||||
|
return pixmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
StatusPill::StatusPill(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(4);
|
||||||
|
|
||||||
|
m_dotLabel = new QLabel(this);
|
||||||
|
m_captionLabel = new QLabel(this);
|
||||||
|
|
||||||
|
layout->addWidget(m_dotLabel);
|
||||||
|
layout->addWidget(m_captionLabel);
|
||||||
|
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||||
|
const QString& caption)
|
||||||
|
{
|
||||||
|
if (dotColor.isValid())
|
||||||
|
{
|
||||||
|
m_dotLabel->setPixmap(renderDot(dotColor, outlineColor));
|
||||||
|
m_dotLabel->show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_dotLabel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_captionLabel->setText(caption);
|
||||||
|
m_captionLabel->setVisible(!caption.isEmpty());
|
||||||
|
setVisible(dotColor.isValid() || !caption.isEmpty());
|
||||||
|
}
|
||||||
29
src/ui/selection/StatusPill.h
Normal file
29
src/ui/selection/StatusPill.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The card header's right slot (REQ-UI-SELECTION-CARD): a colored dot with a short
|
||||||
|
// caption beside it. It carries a building's production status
|
||||||
|
// (REQ-UI-SELECTION-STATUS), a ship's current behavior (REQ-UI-SHIP-BEHAVIOR), or an
|
||||||
|
// aggregated selection's object count (REQ-UI-SELECTION-AGGREGATE) -- never more than
|
||||||
|
// one of them, which is why they share one part.
|
||||||
|
class StatusPill : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit StatusPill(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// An invalid dot color leaves the dot off, for the slots that are a plain caption.
|
||||||
|
// An empty caption hides the pill entirely.
|
||||||
|
void setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||||
|
const QString& caption);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_dotLabel;
|
||||||
|
QLabel* m_captionLabel;
|
||||||
|
};
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
#include "StorageContent.h"
|
#include "StorageContent.h"
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingTarget.h"
|
#include "BuildingTarget.h"
|
||||||
#include "SelectionNames.h"
|
|
||||||
|
|
||||||
StorageContent::StorageContent(const SelectionContext& context,
|
StorageContent::StorageContent(const SelectionContext& context,
|
||||||
const SelectionRequest& request, QWidget* parent)
|
const SelectionRequest& request, QWidget* parent)
|
||||||
@@ -10,20 +8,10 @@ StorageContent::StorageContent(const SelectionContext& context,
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void StorageContent::refreshConfiguration()
|
|
||||||
{
|
|
||||||
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
|
||||||
if (!target.isValid())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
||||||
}
|
|
||||||
|
|
||||||
BufferedBuildingContent::CycleInfo StorageContent::getCycleInfo(
|
BufferedBuildingContent::CycleInfo StorageContent::getCycleInfo(
|
||||||
const Building& /*building*/) const
|
const BuildingTarget& /*target*/) const
|
||||||
{
|
{
|
||||||
// No recipe, no cycle: the card shows the output buffer alone, with no per-cycle
|
// No recipe, no cycle: the card shows the output buffer alone, with no per-cycle
|
||||||
// denominators to show it against (REQ-BLD-SALVAGE-BAY).
|
// denominators and no production progress (REQ-BLD-SALVAGE-BAY).
|
||||||
return CycleInfo();
|
return CycleInfo();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,5 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void refreshConfiguration() override;
|
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||||
CycleInfo getCycleInfo(const Building& building) const override;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user