Files
dota_factory/src/ui/selection/SelectionContent.cpp

226 lines
7.3 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));
// Directly below the header rather than where the runtime group sits, so how far
// along the site is reads before what it is configured to become
// (REQ-UI-SELECTION-CARD).
cardLayout->insertWidget(1, 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));
}