223 lines
7.1 KiB
C++
223 lines
7.1 KiB
C++
#include "SelectionContent.h"
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
|
|
#include <QFont>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#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"
|
|
|
|
namespace
|
|
{
|
|
|
|
// Size of the identity chip in the card header, in device-independent pixels. Smaller
|
|
// than the build button's 32 px chip: here it labels a line of text rather than being
|
|
// the whole button face.
|
|
const int kSymbolSizePx = 20;
|
|
|
|
// Spacing inside the card and between the header's elements.
|
|
const int kCardSpacingPx = 6;
|
|
const int kHeaderSpacingPx = 6;
|
|
|
|
} // namespace
|
|
|
|
|
|
SelectionContent::SelectionContent(const SelectionContext& context,
|
|
std::optional<BuildingId> constructionSiteId,
|
|
QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_context(context)
|
|
, m_siteId(constructionSiteId)
|
|
, m_constructionSection(nullptr)
|
|
, m_constructionBar(nullptr)
|
|
{
|
|
QVBoxLayout* cardLayout = new QVBoxLayout(this);
|
|
cardLayout->setContentsMargins(0, 0, 0, 0);
|
|
cardLayout->setSpacing(kCardSpacingPx);
|
|
|
|
// Header: identity symbol, name, and the right slot pushed to the far edge
|
|
// (REQ-UI-SELECTION-CARD).
|
|
QWidget* header = new QWidget(this);
|
|
QHBoxLayout* headerLayout = new QHBoxLayout(header);
|
|
headerLayout->setContentsMargins(0, 0, 0, 0);
|
|
headerLayout->setSpacing(kHeaderSpacingPx);
|
|
|
|
m_symbolLabel = new QLabel(header);
|
|
m_symbolLabel->hide();
|
|
|
|
m_nameLabel = new QLabel(header);
|
|
QFont nameFont = m_nameLabel->font();
|
|
nameFont.setBold(true);
|
|
m_nameLabel->setFont(nameFont);
|
|
|
|
m_statusPill = new StatusPill(header);
|
|
|
|
headerLayout->addWidget(m_symbolLabel);
|
|
headerLayout->addWidget(m_nameLabel);
|
|
headerLayout->addStretch(1);
|
|
headerLayout->addWidget(m_statusPill);
|
|
cardLayout->addWidget(header);
|
|
|
|
m_configurationGroup = new QWidget(this);
|
|
QVBoxLayout* configurationLayout = new QVBoxLayout(m_configurationGroup);
|
|
configurationLayout->setContentsMargins(0, 0, 0, 0);
|
|
configurationLayout->setSpacing(kCardSpacingPx);
|
|
cardLayout->addWidget(m_configurationGroup);
|
|
|
|
m_runtimeGroup = new QWidget(this);
|
|
QVBoxLayout* runtimeLayout = new QVBoxLayout(m_runtimeGroup);
|
|
runtimeLayout->setContentsMargins(0, 0, 0, 0);
|
|
runtimeLayout->setSpacing(kCardSpacingPx);
|
|
cardLayout->addWidget(m_runtimeGroup);
|
|
|
|
if (m_siteId.has_value())
|
|
{
|
|
// A site has no buffers and runs no production cycle, so whatever the subclass
|
|
// puts into the runtime group is not shown at all; the construction section
|
|
// 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_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"));
|
|
}
|
|
}
|
|
|
|
SelectionContent::~SelectionContent() = default;
|
|
|
|
void SelectionContent::refresh()
|
|
{
|
|
refreshConfiguration();
|
|
if (m_siteId.has_value())
|
|
{
|
|
refreshConstruction();
|
|
return;
|
|
}
|
|
refreshRuntime();
|
|
}
|
|
|
|
QVBoxLayout* SelectionContent::getConfigurationLayout()
|
|
{
|
|
return static_cast<QVBoxLayout*>(m_configurationGroup->layout());
|
|
}
|
|
|
|
QVBoxLayout* SelectionContent::getRuntimeLayout()
|
|
{
|
|
return static_cast<QVBoxLayout*>(m_runtimeGroup->layout());
|
|
}
|
|
|
|
void SelectionContent::setIdentity(const QPixmap& symbol, const QString& name)
|
|
{
|
|
m_symbolLabel->setPixmap(symbol);
|
|
m_symbolLabel->setVisible(!symbol.isNull());
|
|
m_nameLabel->setText(name);
|
|
}
|
|
|
|
void SelectionContent::setBuildingIdentity(BuildingType type, const QString& name)
|
|
{
|
|
const std::string iconName = buildingTypeId(type);
|
|
setIdentity(m_context.buildingIcons->getChip(iconName, kSymbolSizePx), name);
|
|
}
|
|
|
|
void SelectionContent::setSlot(const QColor& dotColor, const QString& caption)
|
|
{
|
|
m_statusPill->setStatus(dotColor, m_context.visuals->statusLight.outline, caption);
|
|
}
|
|
|
|
void SelectionContent::setCountSlot(int count)
|
|
{
|
|
setSlot(QColor(), tr("x%1").arg(count));
|
|
}
|
|
|
|
void SelectionContent::clearSlot()
|
|
{
|
|
setSlot(QColor(), QString());
|
|
}
|
|
|
|
void SelectionContent::setProductionStatusSlot(const Building& building)
|
|
{
|
|
// The classification is the simulation's, so the panel and the world's status light
|
|
// can never disagree (REQ-UI-SELECTION-STATUS, REQ-UI-STATUS-LIGHT).
|
|
const std::optional<ProductionStatus> status =
|
|
getProductionStatus(*m_context.config, building);
|
|
if (!status.has_value())
|
|
{
|
|
clearSlot();
|
|
return;
|
|
}
|
|
|
|
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
|
// The Salvage Bay has no recipe and no cycle: its two states say whether it is
|
|
// holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY).
|
|
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
|
switch (*status)
|
|
{
|
|
case ProductionStatus::Unconfigured:
|
|
setSlot(colors.grey, tr("no recipe"));
|
|
break;
|
|
case ProductionStatus::Producing:
|
|
setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing"));
|
|
break;
|
|
case ProductionStatus::Starved:
|
|
setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input"));
|
|
break;
|
|
case ProductionStatus::Blocked:
|
|
setSlot(colors.yellow, tr("output full"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SelectionContent::refreshConstruction()
|
|
{
|
|
const ConstructionSite* site =
|
|
findSite(m_context.sim->getFactoryState(), *m_siteId);
|
|
if (!site)
|
|
{
|
|
// The site finished or was removed under the card. SelectionPanel rebuilds on
|
|
// the same refresh, so this only has to avoid reading a dead site.
|
|
return;
|
|
}
|
|
|
|
if (site->completesAt == 0)
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
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 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));
|
|
}
|