#include "SelectionContent.h" #include #include #include #include #include #include #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 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(m_configurationGroup->layout()); } QVBoxLayout* SelectionContent::getRuntimeLayout() { return static_cast(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 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(clamped * 100 / duration); m_constructionBar->setValue(static_cast(clamped) / duration, tr("%1%").arg(percent)); }