give the selection cards their own parts instead of label blobs
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
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 <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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
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 <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)));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
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 <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'));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
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 <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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
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 <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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
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 <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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
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 "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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user