give the selection cards their own parts instead of label blobs

This commit is contained in:
2026-08-07 18:43:23 +02:00
parent 2289277e12
commit 075e44d295
54 changed files with 1505 additions and 533 deletions

View File

@@ -10,7 +10,12 @@ set(TARGET_LIB_INCLUDE_DIRS
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
"${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_BALANCING_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/balancing")

View File

@@ -7,6 +7,12 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.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/VisualsLoader.h
# 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}/InspectWindow.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/WorldPrimitives.cpp
PARENT_SCOPE

View File

@@ -45,9 +45,17 @@ bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
}
std::map<std::string, int>
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;
const ShipDef* shipDef = config.ships.findShipDef(b.recipeId);
const ShipDef* shipDef = config.ships.findShipDef(recipeId);
if (!shipDef)
{
return requiredMaterials;
@@ -56,9 +64,9 @@ computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
{
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);
if (!modDef)

View File

@@ -38,6 +38,12 @@ bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
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.
bool hasInputsToStart(const GameConfig& config, const Building& b);

View File

@@ -1,11 +1,14 @@
#include "ShipStatsPanel.h"
#include <QLabel>
#include <QString>
#include <QVBoxLayout>
#include "BarRow.h"
#include "GameConfig.h"
#include "SectionBox.h"
#include "SelectionNames.h"
#include "ShipStatsCalculator.h"
#include "StatRow.h"
#include "ThreatCostCalculator.h"
namespace
@@ -16,20 +19,6 @@ QString fmt(float value)
return QString::number(static_cast<double>(value), 'f', 1);
}
QLabel* makeSectionHeader(const QString& text, QWidget* parent)
{
QLabel* label = new QLabel(text, parent);
QFont f = label->font();
f.setBold(true);
label->setFont(f);
return label;
}
QLabel* makeStatLabel(QWidget* parent)
{
return new QLabel(parent);
}
} // namespace
@@ -38,215 +27,150 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
, m_config(config)
{
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 4, 4, 4);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(2);
layout->setAlignment(Qt::AlignTop);
// Hull stats always visible.
m_hpLabel = makeStatLabel(this);
m_speedLabel = makeStatLabel(this);
m_sensorRangeLabel = makeStatLabel(this);
m_mainAccelLabel = makeStatLabel(this);
m_maneuveringAccelLabel = makeStatLabel(this);
m_angularAccelLabel = makeStatLabel(this);
m_maxRotSpeedLabel = makeStatLabel(this);
m_cargoCapacityLabel = makeStatLabel(this);
m_cargoCapacityLabel->setVisible(false);
// Hull stats -- always visible, except cargo capacity, which only a ship that can
// carry anything shows (REQ-MOD-UI-STATS-PANEL).
m_hpBar = new BarRow(tr("HP"), this);
m_speedRow = new StatRow(tr("Max speed"), this);
m_sensorRangeRow = new StatRow(tr("Sensor range"), this);
m_mainAccelRow = new StatRow(tr("Main accel"), this);
m_maneuveringAccelRow = new StatRow(tr("Maneuvering accel"), this);
m_angularAccelRow = new StatRow(tr("Angular accel"), this);
m_maxRotSpeedRow = new StatRow(tr("Max rotation"), this);
m_cargoCapacityRow = new StatRow(tr("Cargo capacity"), this);
m_cargoCapacityRow->hide();
layout->addWidget(m_hpLabel);
layout->addWidget(m_speedLabel);
layout->addWidget(m_sensorRangeLabel);
layout->addWidget(m_mainAccelLabel);
layout->addWidget(m_maneuveringAccelLabel);
layout->addWidget(m_angularAccelLabel);
layout->addWidget(m_maxRotSpeedLabel);
layout->addWidget(m_cargoCapacityLabel);
layout->addWidget(m_hpBar);
layout->addWidget(m_speedRow);
layout->addWidget(m_sensorRangeRow);
layout->addWidget(m_mainAccelRow);
layout->addWidget(m_maneuveringAccelRow);
layout->addWidget(m_angularAccelRow);
layout->addWidget(m_maxRotSpeedRow);
layout->addWidget(m_cargoCapacityRow);
// Weapon capability section.
m_weaponSection = new QWidget(this);
{
QVBoxLayout* sl = new QVBoxLayout(m_weaponSection);
sl->setContentsMargins(0, 4, 0, 0);
sl->setSpacing(2);
sl->addWidget(makeSectionHeader(tr("Weapons"), m_weaponSection));
m_weaponDpsLabel = makeStatLabel(m_weaponSection);
m_weaponRangeLabel = makeStatLabel(m_weaponSection);
sl->addWidget(m_weaponDpsLabel);
sl->addWidget(m_weaponRangeLabel);
}
m_weaponSection->setVisible(false);
// One section per capability module type, each shown only while at least one such
// module is installed (REQ-MOD-UI-STATS-PANEL).
m_weaponSection = new SectionBox(tr("Weapons"), this);
m_weaponDpsRow = new StatRow(tr("DPS"), m_weaponSection);
m_weaponRangeRow = new StatRow(tr("Range"), m_weaponSection);
m_weaponSection->getContentLayout()->addWidget(m_weaponDpsRow);
m_weaponSection->getContentLayout()->addWidget(m_weaponRangeRow);
m_weaponSection->hide();
layout->addWidget(m_weaponSection);
// Salvage capability section.
m_salvageSection = new QWidget(this);
{
QVBoxLayout* sl = new QVBoxLayout(m_salvageSection);
sl->setContentsMargins(0, 4, 0, 0);
sl->setSpacing(2);
sl->addWidget(makeSectionHeader(tr("Salvage"), m_salvageSection));
m_salvageRateLabel = makeStatLabel(m_salvageSection);
m_salvageRangeLabel = makeStatLabel(m_salvageSection);
sl->addWidget(m_salvageRateLabel);
sl->addWidget(m_salvageRangeLabel);
}
m_salvageSection->setVisible(false);
m_salvageSection = new SectionBox(tr("Salvage"), this);
m_salvageRateRow = new StatRow(tr("Collection rate"), m_salvageSection);
m_salvageRangeRow = new StatRow(tr("Range"), m_salvageSection);
m_salvageSection->getContentLayout()->addWidget(m_salvageRateRow);
m_salvageSection->getContentLayout()->addWidget(m_salvageRangeRow);
m_salvageSection->hide();
layout->addWidget(m_salvageSection);
// Repair capability section.
m_repairSection = new QWidget(this);
{
QVBoxLayout* sl = new QVBoxLayout(m_repairSection);
sl->setContentsMargins(0, 4, 0, 0);
sl->setSpacing(2);
sl->addWidget(makeSectionHeader(tr("Repair"), m_repairSection));
m_repairRateLabel = makeStatLabel(m_repairSection);
m_repairRangeLabel = makeStatLabel(m_repairSection);
sl->addWidget(m_repairRateLabel);
sl->addWidget(m_repairRangeLabel);
}
m_repairSection->setVisible(false);
m_repairSection = new SectionBox(tr("Repair"), this);
m_repairRateRow = new StatRow(tr("Repair rate"), m_repairSection);
m_repairRangeRow = new StatRow(tr("Range"), m_repairSection);
m_repairSection->getContentLayout()->addWidget(m_repairRateRow);
m_repairSection->getContentLayout()->addWidget(m_repairRangeRow);
m_repairSection->hide();
layout->addWidget(m_repairSection);
// Current behavior — live entities only; hidden in the static design
// preview (REQ-UI-SHIP-BEHAVIOR).
m_behaviorLabel = makeSectionHeader(QString(), this);
m_behaviorLabel->setVisible(false);
layout->addWidget(m_behaviorLabel);
// Live entities only; the design preview has no behavior to show
// (REQ-UI-SHIP-BEHAVIOR).
m_behaviorRow = new StatRow(tr("Behavior"), this);
m_behaviorRow->hide();
layout->addWidget(m_behaviorRow);
// Threat cost — debug-only, initially hidden.
m_threatCostLabel = makeStatLabel(this);
m_threatCostLabel->setVisible(false);
layout->addWidget(m_threatCostLabel);
// Threat cost -- shown only while debug draw is active (REQ-UI-SHIP-STATS-PANEL).
m_threatCostRow = new StatRow(tr("Threat cost"), this);
m_threatCostRow->hide();
layout->addWidget(m_threatCostRow);
}
layout->addStretch();
void ShipStatsPanel::setBehavior(BehaviorKind kind)
{
const QString label = getBehaviorLabel(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);
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
applyStats(stats, hpText);
applyStats(stats, 1.0, QString::number(static_cast<int>(stats.hp + 0.5f)));
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);
setThreatCost(calculateShipThreatCost(m_config->threatCosts, *m_config,
shipId, modules));
}
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
{
const QString hpText = tr("HP: %1 / %2")
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));
applyStats(stats, hpText);
.arg(static_cast<int>(stats.hp + 0.5f)));
}
void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText)
void ShipStatsPanel::applyStats(const ShipStats& stats, double hpFraction,
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)));
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)
{
m_cargoCapacityLabel->setText(
tr("Cargo Capacity: %1").arg(stats.cargoCapacity));
m_cargoCapacityLabel->setVisible(true);
}
else
{
m_cargoCapacityLabel->setVisible(false);
m_cargoCapacityRow->setValue(QString::number(stats.cargoCapacity));
}
m_weaponSection->setVisible(stats.weapons.has_value());
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);
m_weaponDpsRow->setValue(fmt(stats.weapons->combinedDps));
m_weaponRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.weapons->maxRange_tiles)));
}
m_salvageSection->setVisible(stats.salvage.has_value());
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);
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_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);
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)));
}
else
{
m_repairSection->setVisible(false);
}
}
void ShipStatsPanel::setBehavior(BehaviorKind kind)
{
QString label;
switch (kind)
{
case BehaviorKind::Retreat: label = tr("Retreating"); break;
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_behaviorLabel->setVisible(false);
return;
}
m_behaviorLabel->setText(tr("Behavior: %1").arg(label));
m_behaviorLabel->setVisible(true);
}
void ShipStatsPanel::setThreatCost(double cost)
{
m_threatCostLabel->setText(tr("Threat Cost: %1").arg(cost, 0, 'f', 1));
m_threatCostLabel->setVisible(m_debugDraw);
m_threatCostRow->setValue(QString::number(cost, 'f', 1));
m_threatCostRow->setVisible(m_debugDraw);
}
void ShipStatsPanel::setDebugDrawEnabled(bool enabled)
{
m_debugDraw = enabled;
m_threatCostLabel->setVisible(m_debugDraw);
m_threatCostRow->setVisible(m_debugDraw);
}

View File

@@ -1,18 +1,28 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include <QWidget>
#include "BehaviorKind.h"
#include "ShipLayout.h"
#include "ShipStatsCalculator.h"
struct GameConfig;
class QLabel;
#include "BehaviorKind.h"
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
{
Q_OBJECT
@@ -20,44 +30,47 @@ class ShipStatsPanel : public QWidget
public:
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,
const std::vector<PlacedModule>& modules);
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 setThreatCost(double cost);
void setDebugDrawEnabled(bool enabled);
private:
void applyStats(const ShipStats& stats, const QString& hpText);
void applyStats(const ShipStats& stats, double hpFraction, const QString& hpText);
const GameConfig* m_config;
bool m_debugDraw = false;
QLabel* m_behaviorLabel;
QLabel* m_hpLabel;
QLabel* m_speedLabel;
QLabel* m_sensorRangeLabel;
QLabel* m_mainAccelLabel;
QLabel* m_maneuveringAccelLabel;
QLabel* m_angularAccelLabel;
QLabel* m_maxRotSpeedLabel;
QLabel* m_cargoCapacityLabel;
BarRow* m_hpBar;
StatRow* m_speedRow;
StatRow* m_sensorRangeRow;
StatRow* m_mainAccelRow;
StatRow* m_maneuveringAccelRow;
StatRow* m_angularAccelRow;
StatRow* m_maxRotSpeedRow;
StatRow* m_cargoCapacityRow;
QWidget* m_weaponSection;
QLabel* m_weaponDpsLabel;
QLabel* m_weaponRangeLabel;
SectionBox* m_weaponSection;
StatRow* m_weaponDpsRow;
StatRow* m_weaponRangeRow;
QWidget* m_salvageSection;
QLabel* m_salvageRateLabel;
QLabel* m_salvageRangeLabel;
SectionBox* m_salvageSection;
StatRow* m_salvageRateRow;
StatRow* m_salvageRangeRow;
QWidget* m_repairSection;
QLabel* m_repairRateLabel;
QLabel* m_repairRangeLabel;
SectionBox* m_repairSection;
StatRow* m_repairRateRow;
StatRow* m_repairRangeRow;
QLabel* m_threatCostLabel;
StatRow* m_behaviorRow;
StatRow* m_threatCostRow;
};

View File

@@ -3,7 +3,6 @@
#include "Building.h"
#include "BuildingTarget.h"
#include "GameConfig.h"
#include "SelectionNames.h"
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
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(
const Building& building) const
const BuildingTarget& target) const
{
CycleInfo info;
// 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
// 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;
if (!building.production.has_value())
if (!target.building || !target.building->production.has_value())
{
return info;
}
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
building.production->recipeId, building.type);
target.building->production->recipeId, target.type);
if (!recipe)
{
return info;

View File

@@ -16,6 +16,5 @@ public:
const SelectionRequest& request, QWidget* parent = nullptr);
protected:
void refreshConfiguration() override;
CycleInfo getCycleInfo(const Building& building) const override;
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
};

118
src/ui/selection/BarRow.cpp Normal file
View 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
View 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;
};

View File

@@ -1,24 +1,17 @@
#include "BufferSection.h"
#include <QLabel>
#include <vector>
#include <QVBoxLayout>
#include "Building.h"
#include "DisplayName.h"
#include "ItemChipRow.h"
#include "SectionBox.h"
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)
{
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
BufferSection::BufferSection(QWidget* parent)
BufferSection::BufferSection(const SelectionContext& context, QWidget* parent)
: QWidget(parent)
{
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->setSpacing(6);
m_label = new QLabel(this);
m_label->setWordWrap(true);
layout->addWidget(m_label);
m_inputSection = new SectionBox(tr("Input buffers"), this);
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
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,
const std::map<std::string, int>& perCycleInputs,
const std::map<std::string, int>& perCycleOutputs)
{
QString text;
if (!building.inputBuffer.counts.empty())
{
text += tr("Input: ");
std::vector<ItemChipRow::Entry> inputs;
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
{
text += formatEntry(entry.first.id, entry.second,
findPerCycle(perCycleInputs, entry.first.id));
ItemChipRow::Entry chip;
chip.itemId = entry.first.id;
chip.countText = QString::number(entry.second);
const int perCycle = findPerCycle(perCycleInputs, entry.first.id);
if (perCycle > 0)
{
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
}
text += "\n";
inputs.push_back(chip);
}
// 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]++;
}
}
// A configured building lists every item its cycle produces, so an output the player
// is waiting for shows as 0 rather than being absent.
// A configured building lists everything its cycle produces, so an output the player
// is waiting for reads as 0 rather than being absent.
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
{
outputCounts.emplace(entry.first, 0);
}
if (!outputCounts.empty())
{
text += tr("Output: ");
std::vector<ItemChipRow::Entry> outputs;
for (const std::pair<const std::string, int>& entry : outputCounts)
{
text += formatEntry(entry.first, entry.second,
findPerCycle(perCycleOutputs, entry.first));
}
ItemChipRow::Entry chip;
chip.itemId = entry.first;
// Counted against the buffer's capacity, which is what production stops at
// (REQ-MAT-OUTPUT-BUFFER).
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());
setVisible(!text.trimmed().isEmpty());
m_inputChips->setEntries(inputs);
m_outputChips->setEntries(outputs);
m_inputSection->setVisible(!inputs.empty());
m_outputSection->setVisible(!outputs.empty());
setVisible(!inputs.empty() || !outputs.empty());
}

View File

@@ -5,20 +5,25 @@
#include <QWidget>
struct Building;
class QLabel;
#include "SelectionContext.h"
// 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
// 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
{
Q_OBJECT
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
// 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);
private:
QLabel* m_label;
SectionBox* m_inputSection;
ItemChipRow* m_inputChips;
SectionBox* m_outputSection;
ItemChipRow* m_outputChips;
};

View File

@@ -1,5 +1,7 @@
#include "BufferedBuildingContent.h"
#include <vector>
#include <QVBoxLayout>
#include "Building.h"
@@ -7,33 +9,76 @@
#include "BuildingTarget.h"
#include "FactoryQueries.h"
#include "ProductionSection.h"
#include "RecipeSummaryRow.h"
#include "SelectionNames.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,
BuildingId id, QWidget* parent)
: SelectionContent(context, asConstructionSite(context, id), parent)
, 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);
getRuntimeLayout()->addWidget(m_buffers);
getRuntimeLayout()->addWidget(m_production);
}
void BufferedBuildingContent::refreshRuntime()
void BufferedBuildingContent::refreshConfiguration()
{
const Building* building = findBuilding(getContext().sim->getFactoryState(), m_id);
if (!building)
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
if (!target.isValid())
{
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
// has to avoid reading it.
return;
}
setProductionStatusSlot(*building);
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
const CycleInfo cycle = getCycleInfo(*building);
m_buffers->setBuffers(*building, cycle.perCycleInputs, cycle.perCycleOutputs);
m_production->setProduction(cycle.runsProduction, *building, cycle.durationSeconds,
const CycleInfo cycle = getCycleInfo(target);
m_recipeSummary->setSummary(toAmounts(cycle.perCycleInputs),
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());
}

View File

@@ -6,15 +6,16 @@
#include "BuildingId.h"
#include "SelectionContent.h"
struct Building;
struct BuildingTarget;
class BufferSection;
class ProductionSection;
class RecipeSummaryRow;
// 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
// (REQ-UI-SELECTION-CONTENT). All four show the same header status, buffer contents and
// production progress; they differ only in what one production cycle costs and how long
// it takes, which is what the subclass supplies.
// (REQ-UI-SELECTION-CONTENT). All four show the same header identity and status, recipe
// summary, buffer contents and production progress; they differ only in what one
// 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
// row of the content catalog.
@@ -40,14 +41,22 @@ protected:
BufferedBuildingContent(const SelectionContext& context, BuildingId id,
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; }
private:
void refreshConfiguration() override;
void refreshRuntime() override;
private:
BuildingId m_id;
RecipeSummaryRow* m_recipeSummary;
BufferSection* m_buffers;
ProductionSection* m_production;
};

View File

@@ -6,6 +6,15 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.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}/ProductionSection.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
@@ -33,6 +42,15 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.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}/ProductionSection.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp

View 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);
}

View 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;
};

View File

@@ -1,18 +1,19 @@
#include "DebrisContent.h"
#include <QLabel>
#include <QVBoxLayout>
#include "DebrisScrap.h"
#include "Simulation.h"
#include "StatRow.h"
DebrisContent::DebrisContent(const SelectionContext& context,
const SelectionRequest& request, QWidget* parent)
: SelectionContent(context, std::nullopt, parent)
, m_debris(request.debris)
{
m_scrapLabel = new QLabel(this);
getRuntimeLayout()->addWidget(m_scrapLabel);
m_scrapRow = new StatRow(tr("Scrap remaining"), this);
m_scrapRow->setValueEmphasized(true);
getRuntimeLayout()->addWidget(m_scrapRow);
setIdentity(QPixmap(), tr("Debris"));
if (m_debris.size() > 1)
@@ -24,7 +25,8 @@ DebrisContent::DebrisContent(const SelectionContext& context,
void DebrisContent::refreshRuntime()
{
// 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.
m_scrapLabel->setText(tr("Scrap remaining: %1")
.arg(sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
// (REQ-UI-DEBRIS-CLICK-SELECT), so it is re-summed rather than remembered. With
// several pieces selected it is their sum (REQ-UI-SELECTION-AGGREGATE).
m_scrapRow->setValue(QString::number(
sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
}

View File

@@ -7,7 +7,7 @@
#include "SelectionContent.h"
#include "SelectionContentFactory.h"
class QLabel;
class StatRow;
// The card for selected debris (REQ-UI-DEBRIS-PANEL): the scrap still left in it.
//
@@ -27,5 +27,5 @@ protected:
private:
std::vector<entt::entity> m_debris;
QLabel* m_scrapLabel;
StatRow* m_scrapRow;
};

View 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);
}

View 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);
};

View File

@@ -3,33 +3,31 @@
#include <map>
#include <string>
#include <QLabel>
#include <QStringList>
#include <QVBoxLayout>
#include "CountRow.h"
#include "DebrisScrap.h"
#include "DisplayName.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "ShipIdentityComponent.h"
#include "Simulation.h"
#include "StatRow.h"
#include "StationBodyComponent.h"
FieldMultiContent::FieldMultiContent(const SelectionContext& context,
const SelectionRequest& request, QWidget* parent)
: SelectionContent(context, std::nullopt, parent)
, m_actors(request.actors)
, 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"));
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();
@@ -39,7 +37,7 @@ void FieldMultiContent::refreshRuntime()
std::map<QString, int> counts;
std::map<QString, QString> labels;
for (entt::entity actor : m_actors)
for (entt::entity actor : actors)
{
if (!admin.isValid(actor)) { continue; }
const bool isEnemy = admin.hasAll<FactionComponent>(actor)
@@ -75,17 +73,33 @@ void FieldMultiContent::refreshRuntime()
counts[key] += 1;
}
QStringList lines;
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())
{
lines << tr("Debris x %1").arg(static_cast<int>(m_debris.size()));
// The scrap total follows the debris row rather than standing on its own, so it
// reads as belonging to it (REQ-UI-DEBRIS-PANEL).
lines << tr(" holding %1 scrap").arg(sumDebrisScrap(admin, m_debris));
getRuntimeLayout()->addWidget(new CountRow(
QPixmap(), tr("Debris"), static_cast<int>(m_debris.size()), this));
// 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'));
}

View File

@@ -7,7 +7,7 @@
#include "SelectionContent.h"
#include "SelectionContentFactory.h"
class QLabel;
class StatRow;
// 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
@@ -24,7 +24,10 @@ protected:
void refreshRuntime() override;
private:
std::vector<entt::entity> m_actors;
void buildSummary(const std::vector<entt::entity>& actors);
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;
};

View File

@@ -1,11 +1,16 @@
#include "HqContent.h"
#include <QLabel>
#include <vector>
#include <QVBoxLayout>
#include "BarRow.h"
#include "EntityAdmin.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "IconCaption.h"
#include "ItemChipRow.h"
#include "SectionBox.h"
#include "SelectionNames.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.
: SelectionContent(context, std::nullopt, parent)
{
m_stockLabel = new QLabel(this);
m_hpLabel = new QLabel(this);
getRuntimeLayout()->addWidget(m_stockLabel);
getRuntimeLayout()->addWidget(m_hpLabel);
m_stockSection = new SectionBox(tr("Building blocks"), this);
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
m_stockSection->getContentLayout()->addWidget(m_stockChips);
m_hpBar = new BarRow(tr("HP"), this);
getRuntimeLayout()->addWidget(m_stockSection);
getRuntimeLayout()->addWidget(m_hpBar);
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
}
void HqContent::refreshRuntime()
{
m_stockLabel->setText(
tr("Building blocks: %1").arg(getContext().sim->getBuildingBlocksStock()));
// Not a buffer: blocks delivered by belt go straight into the global stock
// (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
// (REQ-HQ-STATS, REQ-UI-HP-BARS).
@@ -35,7 +50,10 @@ void HqContent::refreshRuntime()
[this](entt::entity /*entity*/, const HqProxyComponent& /*proxy*/,
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.maxHp + 0.5f)));
});

View File

@@ -3,7 +3,9 @@
#include "SelectionContent.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
// HP. It has no configuration group and no status indicator.
@@ -24,6 +26,7 @@ protected:
void refreshRuntime() override;
private:
QLabel* m_stockLabel;
QLabel* m_hpLabel;
SectionBox* m_stockSection;
ItemChipRow* m_stockChips;
BarRow* m_hpBar;
};

View 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());
}

View 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;
};

View 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);
}
}

View 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;
};

View File

@@ -2,16 +2,26 @@
#include <map>
#include <QLabel>
#include <QStringList>
#include <QVBoxLayout>
#include "Building.h"
#include "BuildingIconCache.h"
#include "ClearBeltControl.h"
#include "CountRow.h"
#include "FactoryQueries.h"
#include "GameConfig.h"
#include "SelectionNames.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,
const SelectionRequest& request,
@@ -19,10 +29,11 @@ MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
: SelectionContent(context, std::nullopt, parent)
, m_ids(request.buildings)
{
m_countsLabel = new QLabel(this);
m_totalCostLabel = new QLabel(this);
getRuntimeLayout()->addWidget(m_countsLabel);
getRuntimeLayout()->addWidget(m_totalCostLabel);
// 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();
// 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.
@@ -40,12 +51,6 @@ MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
{
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()
@@ -68,11 +73,13 @@ void MultiBuildingContent::buildSummary()
}
}
QStringList lines;
int totalCost = 0;
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
// 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'));
m_totalCostLabel->setText(tr("Total: %1 Building Blocks").arg(totalCost));
StatRow* totalRow = new StatRow(tr("Total cost"), this);
totalRow->setValue(QString::number(totalCost));
totalRow->setValueEmphasized(true);
getRuntimeLayout()->addWidget(totalRow);
}

View File

@@ -6,7 +6,6 @@
#include "SelectionContent.h"
#include "SelectionContentFactory.h"
class QLabel;
// 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
@@ -29,6 +28,4 @@ private:
void buildSummary();
std::vector<BuildingId> m_ids;
QLabel* m_countsLabel;
QLabel* m_totalCostLabel;
};

View File

@@ -2,10 +2,11 @@
#include <algorithm>
#include <QLabel>
#include <QVBoxLayout>
#include "BarRow.h"
#include "Building.h"
#include "SectionBox.h"
ProductionSection::ProductionSection(QWidget* parent)
: QWidget(parent)
@@ -14,40 +15,34 @@ ProductionSection::ProductionSection(QWidget* parent)
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_label = new QLabel(this);
layout->addWidget(m_label);
m_section = new SectionBox(tr("Production"), this);
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,
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).
if (!runsProduction)
{
hide();
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();
// 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));
}

View File

@@ -5,7 +5,8 @@
#include "Tick.h"
struct Building;
class QLabel;
class BarRow;
class SectionBox;
// The cycle time and production progress of one building (REQ-UI-PRODUCTION-PROGRESS).
//
@@ -29,5 +30,6 @@ public:
double durationSeconds, Tick currentTick);
private:
QLabel* m_label;
SectionBox* m_section;
BarRow* m_bar;
};

View File

@@ -2,11 +2,9 @@
#include <QVBoxLayout>
#include "Building.h"
#include "BuildingTarget.h"
#include "GameConfig.h"
#include "RecipeSelectionControl.h"
#include "SelectionNames.h"
RecipeProductionContent::RecipeProductionContent(const SelectionContext& context,
const SelectionRequest& request,
@@ -15,31 +13,25 @@ RecipeProductionContent::RecipeProductionContent(const SelectionContext& context
{
const BuildingTarget target = resolveBuildingTarget(context, getBuildingId());
// The control is shown for a construction site too: a site is configured exactly
// like the building it will become (REQ-BLD-SITE-CONFIG).
// Shown for a construction site too: a site is configured exactly like the building
// it will become (REQ-BLD-SITE-CONFIG).
m_recipeControl = new RecipeSelectionControl(context, getBuildingId(), target.type,
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);
}
BufferedBuildingContent::CycleInfo RecipeProductionContent::getCycleInfo(
const Building& building) const
const BuildingTarget& target) const
{
CycleInfo info;
const RecipeDef* recipe = building.recipeId.empty()
const RecipeDef* recipe = target.recipeId.empty()
? nullptr
: getContext().config->recipes.findRecipeDef(building.recipeId, building.type);
: getContext().config->recipes.findRecipeDef(target.recipeId, target.type);
if (!recipe)
{
return info;

View File

@@ -18,8 +18,8 @@ public:
const SelectionRequest& request, QWidget* parent = nullptr);
protected:
void refreshConfiguration() override;
CycleInfo getCycleInfo(const Building& building) const override;
void refreshControls(const BuildingTarget& target) override;
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
private:
RecipeSelectionControl* m_recipeControl;

View 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));
}
}

View 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;
};

View 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;
}

View 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;
};

View File

@@ -4,18 +4,19 @@
#include <string>
#include <QFont>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QRectF>
#include <QVBoxLayout>
#include "BarRow.h"
#include "BuildingIconCache.h"
#include "EmptyNote.h"
#include "FactoryQueries.h"
#include "GameConfig.h"
#include "ProductionRules.h"
#include "SectionBox.h"
#include "Simulation.h"
#include "StatusPill.h"
#include "Tick.h"
#include "VisualsConfig.h"
@@ -27,31 +28,10 @@ namespace
// the whole button face.
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.
const int kCardSpacingPx = 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
@@ -61,7 +41,8 @@ SelectionContent::SelectionContent(const SelectionContext& context,
: QWidget(parent)
, m_context(context)
, m_siteId(constructionSiteId)
, m_constructionLabel(nullptr)
, m_constructionSection(nullptr)
, m_constructionBar(nullptr)
{
QVBoxLayout* cardLayout = new QVBoxLayout(this);
cardLayout->setContentsMargins(0, 0, 0, 0);
@@ -82,17 +63,12 @@ SelectionContent::SelectionContent(const SelectionContext& context,
nameFont.setBold(true);
m_nameLabel->setFont(nameFont);
m_slotDot = new QLabel(header);
m_slotDot->hide();
m_slotLabel = new QLabel(header);
m_slotLabel->hide();
m_statusPill = new StatusPill(header);
headerLayout->addWidget(m_symbolLabel);
headerLayout->addWidget(m_nameLabel);
headerLayout->addStretch(1);
headerLayout->addWidget(m_slotDot);
headerLayout->addWidget(m_slotLabel);
headerLayout->addWidget(m_statusPill);
cardLayout->addWidget(header);
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
// still fills the group -- it just never becomes visible.
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"));
}
@@ -159,18 +140,7 @@ void SelectionContent::setBuildingIdentity(BuildingType type, const QString& nam
void SelectionContent::setSlot(const QColor& dotColor, const QString& caption)
{
if (dotColor.isValid())
{
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());
m_statusPill->setStatus(dotColor, m_context.visuals->statusLight.outline, caption);
}
void SelectionContent::setCountSlot(int count)
@@ -227,28 +197,26 @@ void SelectionContent::refreshConstruction()
return;
}
QString progress;
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_context.config->buildings.findBuildingDef(site->type);
if (!def || def->constructionTimeSeconds <= 0)
{
m_constructionBar->setValue(0.0, tr("Building..."));
return;
}
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 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));
}

View File

@@ -12,8 +12,11 @@
#include "SelectionContext.h"
struct Building;
class BarRow;
class QLabel;
class QVBoxLayout;
class SectionBox;
class StatusPill;
// 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
@@ -90,12 +93,12 @@ private:
QLabel* m_symbolLabel;
QLabel* m_nameLabel;
QLabel* m_slotDot;
QLabel* m_slotLabel;
StatusPill* m_statusPill;
QWidget* m_configurationGroup;
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.
QLabel* m_constructionLabel;
SectionBox* m_constructionSection;
BarRow* m_constructionBar;
};

View File

@@ -14,3 +14,22 @@ QString getBuildingTypeName(BuildingType 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();
}

View File

@@ -2,9 +2,14 @@
#include <QString>
#include "BehaviorKind.h"
#include "BuildingType.h"
// 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
// config id, so a new building type needs no entry here.
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);

View File

@@ -6,6 +6,7 @@
#include "GameConfig.h"
#include "HealthComponent.h"
#include "SelectedBehaviorComponent.h"
#include "SelectionNames.h"
#include "ShipIdentityComponent.h"
#include "ShipStatsCalculator.h"
#include "ShipStatsPanel.h"
@@ -47,9 +48,13 @@ void ShipContent::refreshRuntime()
const ShipStats stats = buildShipStatsFromEntity(admin, m_entity);
m_statsPanel->refreshFromLive(stats, health.hp);
m_statsPanel->setBehavior(admin.get<SelectedBehaviorComponent>(m_entity).winner);
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 ShipDef* schematicDef =
getContext().config->ships.findShipDef(identity.schematicId);

View File

@@ -3,14 +3,13 @@
#include <QPushButton>
#include <QVBoxLayout>
#include "Building.h"
#include "BuildingTarget.h"
#include "EventManager.h"
#include "GameConfig.h"
#include "LayoutDialogRequestedEvent.h"
#include "ProductionRules.h"
#include "RecipeSelectionControl.h"
#include "SelectionNames.h"
#include "SectionBox.h"
#include "ShipLayoutPreview.h"
ShipyardContent::ShipyardContent(const SelectionContext& context,
@@ -19,12 +18,17 @@ ShipyardContent::ShipyardContent(const SelectionContext& context,
{
m_schematicControl = new RecipeSelectionControl(context, getBuildingId(),
BuildingType::Shipyard, this);
m_layoutPreview = new ShipLayoutPreview(this);
m_configureButton = new QPushButton(tr("Configure Layout"), this);
getConfigurationLayout()->addWidget(m_schematicControl);
getConfigurationLayout()->addWidget(m_layoutPreview);
getConfigurationLayout()->addWidget(m_configureButton);
m_layoutSection = new SectionBox(tr("Layout"), this);
m_layoutPreview = new ShipLayoutPreview(m_layoutSection);
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();
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);
// 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(
const Building& building) const
const BuildingTarget& target) const
{
CycleInfo info;
const ShipDef* shipDef = building.recipeId.empty()
const ShipDef* shipDef = target.recipeId.empty()
? nullptr
: getContext().config->ships.findShipDef(building.recipeId);
: getContext().config->ships.findShipDef(target.recipeId);
if (!shipDef)
{
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
// input buffers (REQ-BLD-SHIPYARD). The simulation owns that sum, so the panel asks
// it rather than adding the modules up a second time.
info.perCycleInputs =
computeShipyardRequiredMaterials(*getContext().config, building);
info.perCycleInputs = computeShipyardRequiredMaterials(
*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;
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 =
getContext().config->modules.findModuleDef(placed.moduleId);

View File

@@ -5,6 +5,7 @@
class QPushButton;
class RecipeSelectionControl;
class SectionBox;
class ShipLayoutPreview;
// The card for a Shipyard (REQ-UI-SELECTION-CONTENT): the schematic selection, the
@@ -22,11 +23,12 @@ public:
QWidget* parent = nullptr);
protected:
void refreshConfiguration() override;
CycleInfo getCycleInfo(const Building& building) const override;
void refreshControls(const BuildingTarget& target) override;
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
private:
RecipeSelectionControl* m_schematicControl;
SectionBox* m_layoutSection;
ShipLayoutPreview* m_layoutPreview;
QPushButton* m_configureButton;
};

View 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);
}

View 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;
};

View File

@@ -1,13 +1,14 @@
#include "StationContent.h"
#include <QLabel>
#include <QVBoxLayout>
#include "BarRow.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "Simulation.h"
#include "StatRow.h"
#include "WeaponComponent.h"
StationContent::StationContent(const SelectionContext& context,
@@ -15,9 +16,15 @@ StationContent::StationContent(const SelectionContext& context,
: SelectionContent(context, std::nullopt, parent)
, m_entity(request.actors.front())
{
m_statsLabel = new QLabel(this);
m_statsLabel->setWordWrap(true);
getRuntimeLayout()->addWidget(m_statsLabel);
m_hpBar = new BarRow(tr("HP"), this);
m_damageRow = new StatRow(tr("Damage"), this);
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();
const bool isEnemy = admin.isValid(m_entity)
@@ -36,10 +43,18 @@ void StationContent::refreshRuntime()
}
const HealthComponent& health = admin.get<HealthComponent>(m_entity);
// A station's weapons are child module entities pointing back at it, so its combined
// damage and range are summed over those rather than read off the station itself.
float totalDps = 0.0f;
const double hpFraction = (health.maxHp > 0.0f)
? static_cast<double>(health.hp) / health.maxHp
: 0.0;
m_hpBar->setValue(hpFraction, tr("%1 / %2")
.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;
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
@@ -48,19 +63,21 @@ void StationContent::refreshRuntime()
{
if (owner.owner != station) { return; }
hasWeapon = true;
totalDps += weapon.damage * weapon.fireRateHz;
totalDamage += weapon.damage;
if (weapon.range_tiles > maxRange) { maxRange = weapon.range_tiles; }
if (weapon.fireRateHz > maxFireRateHz) { maxFireRateHz = weapon.fireRateHz; }
});
QString text = tr("HP: %1 / %2")
.arg(static_cast<int>(health.hp + 0.5f))
.arg(static_cast<int>(health.maxHp + 0.5f));
m_damageRow->setVisible(hasWeapon);
m_rangeRow->setVisible(hasWeapon);
m_fireRateRow->setVisible(hasWeapon);
if (hasWeapon)
{
text += tr("\nDPS: %1")
.arg(QString::number(static_cast<double>(totalDps), 'f', 1));
text += tr("\nRange: %1 tiles")
.arg(QString::number(static_cast<double>(maxRange), 'f', 1));
m_damageRow->setValue(
QString::number(static_cast<double>(totalDamage), 'f', 1));
m_rangeRow->setValue(tr("%1 tiles")
.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);
}

View File

@@ -5,7 +5,8 @@
#include "SelectionContent.h"
#include "SelectionContentFactory.h"
class QLabel;
class BarRow;
class StatRow;
// 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
@@ -23,5 +24,8 @@ protected:
private:
entt::entity m_entity;
QLabel* m_statsLabel;
BarRow* m_hpBar;
StatRow* m_damageRow;
StatRow* m_rangeRow;
StatRow* m_fireRateRow;
};

View 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());
}

View 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;
};

View File

@@ -1,8 +1,6 @@
#include "StorageContent.h"
#include "Building.h"
#include "BuildingTarget.h"
#include "SelectionNames.h"
StorageContent::StorageContent(const SelectionContext& context,
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(
const Building& /*building*/) const
const BuildingTarget& /*target*/) const
{
// 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();
}

View File

@@ -17,6 +17,5 @@ public:
QWidget* parent = nullptr);
protected:
void refreshConfiguration() override;
CycleInfo getCycleInfo(const Building& building) const override;
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
};