Files
dota_factory/src/ui/SelectedBuildingPanel.cpp
2026-05-18 08:54:26 +02:00

683 lines
19 KiB
C++

#include "SelectedBuildingPanel.h"
#include <algorithm>
#include <cctype>
#include <map>
#include <set>
#include <string>
#include <QComboBox>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ItemType.h"
#include "ModulesConfig.h"
#include "Rotation.h"
#include "ShipLayoutPreview.h"
#include "Simulation.h"
namespace
{
QString buildingTypeName(BuildingType type)
{
const std::string id = buildingTypeId(type);
QString result;
bool nextUpper = true;
for (char c : id)
{
if (c == '_')
{
result += ' ';
nextUpper = true;
}
else if (nextUpper)
{
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
nextUpper = false;
}
else
{
result += c;
}
}
return result;
}
bool isProductionBuilding(BuildingType type)
{
return type == BuildingType::Miner
|| type == BuildingType::Smelter
|| type == BuildingType::Assembler
|| type == BuildingType::ReprocessingPlant
|| type == BuildingType::Shipyard;
}
bool isBeltLike(BuildingType type)
{
return type == BuildingType::Belt || type == BuildingType::Splitter
|| type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit;
}
QString rotationLabel(Rotation r)
{
switch (r)
{
case Rotation::North: return "North (↑)";
case Rotation::East: return "East (→)";
case Rotation::South: return "South (↓)";
case Rotation::West: return "West (←)";
}
return "";
}
} // namespace
SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
const GameConfig* config,
QWidget* parent)
: QWidget(parent)
, m_sim(sim)
, m_config(config)
, m_singleId(kInvalidEntityId)
, m_splitterTile(0, 0)
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(8, 8, 8, 8);
m_layout->setSpacing(4);
m_layout->setAlignment(Qt::AlignTop);
m_titleLabel = new QLabel(this);
m_recipeCombo = new QComboBox(this);
m_clearBeltBtn = new QPushButton("Clear Items", this);
m_filterALabel = new QLabel(this);
m_filterAList = new QListWidget(this);
m_filterBLabel = new QLabel(this);
m_filterBList = new QListWidget(this);
m_layoutPreview = new ShipLayoutPreview(this);
m_configureLayoutBtn = new QPushButton("Configure Layout", this);
m_buffersLabel = new QLabel(this);
m_buffersLabel->setWordWrap(true);
m_filterAList->setMaximumHeight(100);
m_filterBList->setMaximumHeight(100);
m_layout->addWidget(m_titleLabel);
m_layout->addWidget(m_recipeCombo);
m_layout->addWidget(m_layoutPreview);
m_layout->addWidget(m_configureLayoutBtn);
m_layout->addWidget(m_clearBeltBtn);
m_layout->addWidget(m_filterALabel);
m_layout->addWidget(m_filterAList);
m_layout->addWidget(m_filterBLabel);
m_layout->addWidget(m_filterBList);
m_layout->addWidget(m_buffersLabel);
connect(m_recipeCombo, qOverload<int>(&QComboBox::currentIndexChanged),
this, &SelectedBuildingPanel::onRecipeChanged);
connect(m_clearBeltBtn, &QPushButton::clicked,
this, &SelectedBuildingPanel::onClearBelt);
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
if (m_singleId != kInvalidEntityId)
{
emit layoutDialogRequested(m_singleId);
}
});
connect(m_filterAList, &QListWidget::itemChanged,
this, &SelectedBuildingPanel::onSplitterFilterChanged);
connect(m_filterBList, &QListWidget::itemChanged,
this, &SelectedBuildingPanel::onSplitterFilterChanged);
buildEmpty();
}
void SelectedBuildingPanel::onSelectionChanged(const std::vector<EntityId>& ids)
{
m_selection = ids;
rebuild();
}
void SelectedBuildingPanel::rebuild()
{
if (m_selection.empty())
{
buildEmpty();
}
else if (m_selection.size() == 1)
{
buildSingle(m_selection[0]);
}
else
{
buildMulti(m_selection);
}
}
void SelectedBuildingPanel::buildEmpty()
{
m_singleId = kInvalidEntityId;
m_titleLabel->hide();
m_recipeCombo->hide();
m_layoutPreview->hide();
m_configureLayoutBtn->hide();
m_clearBeltBtn->hide();
m_filterALabel->hide();
m_filterAList->hide();
m_filterBLabel->hide();
m_filterBList->hide();
m_buffersLabel->hide();
}
void SelectedBuildingPanel::buildSingle(EntityId id)
{
m_singleId = id;
const Building* b = m_sim->buildings().findBuilding(id);
if (!b)
{
const ConstructionSite* s = m_sim->buildings().findSite(id);
if (!s)
{
buildEmpty();
return;
}
QString progress;
if (s->completesAt == 0)
{
progress = "Queued";
}
else
{
const BuildingDef* def = nullptr;
for (const BuildingDef& d : m_config->buildings.buildings)
{
if (d.type == s->type) { def = &d; break; }
}
if (def && def->constructionTimeSeconds > 0)
{
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = QString::number(pct) + "% complete";
}
else
{
progress = "Building...";
}
}
m_titleLabel->setText("(Building) " + buildingTypeName(s->type));
m_titleLabel->show();
m_buffersLabel->setText(progress);
m_buffersLabel->show();
return;
}
m_titleLabel->setText(buildingTypeName(b->type));
m_titleLabel->show();
m_buffersLabel->show();
if (isProductionBuilding(b->type))
{
m_recipeCombo->blockSignals(true);
m_recipeCombo->clear();
m_recipeCombo->addItem("(none)", QString());
if (b->type == BuildingType::Shipyard)
{
for (const ShipDef& def : m_config->ships.ships)
{
if (m_sim->isSchematicUnlocked(def.id))
{
m_recipeCombo->addItem(
QString::fromStdString(def.id),
QString::fromStdString(def.id));
}
}
}
else
{
for (const RecipeDef& recipe : m_config->recipes.recipes)
{
if (recipe.building == b->type)
{
m_recipeCombo->addItem(
QString::fromStdString(recipe.id),
QString::fromStdString(recipe.id));
}
}
}
const int currentIdx = m_recipeCombo->findData(QString::fromStdString(b->recipeId));
m_recipeCombo->setCurrentIndex(currentIdx >= 0 ? currentIdx : 0);
m_recipeCombo->blockSignals(false);
m_recipeCombo->show();
if (b->type == BuildingType::Shipyard && !b->recipeId.empty())
{
const ShipDef* sDef = findShipDef(b->recipeId);
if (sDef && !sDef->layout.empty())
{
ShipLayoutConfig layout;
if (b->shipLayout.has_value())
{
layout = *b->shipLayout;
}
m_layoutPreview->setShipAndLayout(
sDef->layout, layout, &m_config->modules.modules);
m_layoutPreview->show();
m_configureLayoutBtn->show();
}
else
{
m_layoutPreview->hide();
m_configureLayoutBtn->hide();
}
}
else
{
m_layoutPreview->hide();
m_configureLayoutBtn->hide();
}
}
else
{
m_recipeCombo->hide();
m_layoutPreview->hide();
m_configureLayoutBtn->hide();
}
if (isBeltLike(b->type))
{
m_clearBeltBtn->show();
}
else
{
m_clearBeltBtn->hide();
}
if (b->type == BuildingType::Splitter)
{
m_splitterTile = b->anchor;
buildSplitterFilters(m_splitterTile);
}
else
{
m_filterALabel->hide();
m_filterAList->hide();
m_filterBLabel->hide();
m_filterBList->hide();
}
refreshBuffers(b);
}
void SelectedBuildingPanel::refreshBuffers(const Building* b)
{
const RecipeDef* recipe = findRecipe(b);
const ShipDef* shipDef = (b->type == BuildingType::Shipyard)
? findShipDef(b->recipeId)
: nullptr;
QString bufText;
if (!b->inputBuffer.counts.empty())
{
bufText += "Input: ";
for (const std::pair<const ItemType, int>& entry : b->inputBuffer.counts)
{
int perCycle = 0;
if (recipe)
{
for (const RecipeIngredient& ing : recipe->inputs)
{
if (ing.item == entry.first.id) { perCycle = ing.amount; break; }
}
}
else if (shipDef)
{
for (const RecipeIngredient& mat : shipDef->schematic.materials)
{
if (mat.item == entry.first.id) { perCycle = mat.amount; break; }
}
if (b->shipLayout.has_value())
{
for (const PlacedModule& pm : b->shipLayout->placedModules)
{
for (const ModuleDef& modDef : m_config->modules.modules)
{
if (modDef.id == pm.moduleId)
{
for (const RecipeIngredient& ing : modDef.materials)
{
if (ing.item == entry.first.id)
{
perCycle += ing.amount;
}
}
break;
}
}
}
}
}
bufText += QString::fromStdString(entry.first.id)
+ ": " + QString::number(entry.second);
if (perCycle > 0)
{
bufText += "/" + QString::number(perCycle);
}
bufText += " ";
}
bufText += "\n";
}
if (recipe && !recipe->outputs.empty())
{
std::map<std::string, int> outCounts;
for (const Item& item : b->outputBuffer.items)
{
outCounts[item.type.id]++;
}
bufText += "Output: ";
for (const RecipeOutput& out : recipe->outputs)
{
const std::map<std::string, int>::const_iterator it =
outCounts.find(out.item);
const int count = (it != outCounts.end()) ? it->second : 0;
bufText += QString::fromStdString(out.item)
+ ": " + QString::number(count)
+ "/" + QString::number(out.amount) + " ";
}
}
else if (!b->outputBuffer.items.empty())
{
std::map<std::string, int> outCounts;
for (const Item& item : b->outputBuffer.items)
{
outCounts[item.type.id]++;
}
bufText += "Output: ";
for (const std::pair<const std::string, int>& entry : outCounts)
{
bufText += QString::fromStdString(entry.first)
+ ": " + QString::number(entry.second) + " ";
}
}
if (isProductionBuilding(b->type) && (recipe || shipDef))
{
double durationSeconds = recipe
? recipe->durationSeconds
: shipDef->schematic.productionTimeSeconds;
if (shipDef && b->shipLayout.has_value())
{
for (const PlacedModule& pm : b->shipLayout->placedModules)
{
for (const ModuleDef& modDef : m_config->modules.modules)
{
if (modDef.id == pm.moduleId)
{
durationSeconds += modDef.productionTimeSeconds;
break;
}
}
}
}
bufText += QString("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
if (b->production.has_value())
{
const Tick cycleTicks = secondsToTicks(durationSeconds);
const Tick completesAt = b->production->completesAt;
const Tick currentTick = m_sim->currentTick();
const Tick elapsed = currentTick - (completesAt - cycleTicks);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
bufText += QString("Progress: %1%\n").arg(pct);
}
else
{
bufText += "Progress: idle\n";
}
}
m_buffersLabel->setText(bufText);
if (b->type == BuildingType::Shipyard && shipDef && !shipDef->layout.empty())
{
ShipLayoutConfig layout;
if (b->shipLayout.has_value())
{
layout = *b->shipLayout;
}
m_layoutPreview->setShipAndLayout(
shipDef->layout, layout, &m_config->modules.modules);
}
}
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
{
if (b->recipeId.empty()) { return nullptr; }
for (const RecipeDef& r : m_config->recipes.recipes)
{
if (r.id == b->recipeId && r.building == b->type) { return &r; }
}
return nullptr;
}
const ShipDef* SelectedBuildingPanel::findShipDef(const std::string& id) const
{
if (id.empty()) { return nullptr; }
for (const ShipDef& s : m_config->ships.ships)
{
if (s.id == id) { return &s; }
}
return nullptr;
}
void SelectedBuildingPanel::onStateUpdated(Tick /*tick*/, int /*blocks*/, double /*speed*/)
{
if (m_singleId == kInvalidEntityId) { return; }
const Building* b = m_sim->buildings().findBuilding(m_singleId);
if (b)
{
// If the panel was last showing this id as a construction site, the
// full building UI (recipe combo, ports, etc.) hasn't been built yet.
if (m_titleLabel->text().startsWith("(Building) "))
{
rebuild();
}
else
{
refreshBuffers(b);
}
return;
}
const ConstructionSite* s = m_sim->buildings().findSite(m_singleId);
if (s)
{
rebuild();
return;
}
buildEmpty();
}
void SelectedBuildingPanel::buildMulti(const std::vector<EntityId>& ids)
{
m_singleId = kInvalidEntityId;
m_recipeCombo->hide();
m_clearBeltBtn->hide();
m_filterALabel->hide();
m_filterAList->hide();
m_filterBLabel->hide();
m_filterBList->hide();
m_buffersLabel->hide();
std::map<BuildingType, int> counts;
for (EntityId id : ids)
{
const Building* b = m_sim->buildings().findBuilding(id);
if (b)
{
counts[b->type]++;
continue;
}
const ConstructionSite* s = m_sim->buildings().findSite(id);
if (s)
{
counts[s->type]++;
}
}
bool hasBelt = false;
QString text;
for (const std::pair<const BuildingType, int>& entry : counts)
{
text += buildingTypeName(entry.first) + ": "
+ QString::number(entry.second) + "\n";
if (isBeltLike(entry.first))
{
hasBelt = true;
}
}
m_titleLabel->setText(text.trimmed());
m_titleLabel->show();
if (hasBelt)
{
m_clearBeltBtn->show();
}
}
void SelectedBuildingPanel::onRecipeChanged(int comboIndex)
{
if (m_singleId == kInvalidEntityId)
{
return;
}
const QString recipeId = m_recipeCombo->itemData(comboIndex).toString();
m_sim->buildings().setRecipe(m_singleId, recipeId.toStdString());
rebuild();
}
void SelectedBuildingPanel::buildSplitterFilters(QPoint splitterTile)
{
const std::optional<BeltSystem::SplitterInfo> info =
m_sim->belts().getSplitterInfo(splitterTile);
if (!info.has_value())
{
m_filterALabel->hide();
m_filterAList->hide();
m_filterBLabel->hide();
m_filterBList->hide();
return;
}
const std::vector<std::string> items = allItemIds();
auto populateList = [&](QListWidget* list, QLabel* label,
const QString& dirLabel,
const std::vector<ItemType>& filter)
{
label->setText(dirLabel + " filter (empty = all):");
list->blockSignals(true);
list->clear();
for (const std::string& itemId : items)
{
QListWidgetItem* row = new QListWidgetItem(
QString::fromStdString(itemId), list);
const bool checked = filter.empty()
? false
: std::find(filter.begin(), filter.end(),
ItemType{itemId}) != filter.end();
row->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
row->setFlags(row->flags() | Qt::ItemIsUserCheckable);
}
list->blockSignals(false);
label->show();
list->show();
};
populateList(m_filterAList, m_filterALabel,
rotationLabel(info->outputA), info->filterA);
populateList(m_filterBList, m_filterBLabel,
rotationLabel(info->outputB), info->filterB);
}
void SelectedBuildingPanel::onSplitterFilterChanged()
{
if (m_singleId == kInvalidEntityId)
{
return;
}
auto collectFilter = [](QListWidget* list) -> std::vector<ItemType>
{
std::vector<ItemType> filter;
for (int i = 0; i < list->count(); ++i)
{
const QListWidgetItem* row = list->item(i);
if (row->checkState() == Qt::Checked)
{
filter.push_back(ItemType{row->text().toStdString()});
}
}
return filter;
};
m_sim->belts().setSplitterFilters(
m_splitterTile,
collectFilter(m_filterAList),
collectFilter(m_filterBList));
}
std::vector<std::string> SelectedBuildingPanel::allItemIds() const
{
std::set<std::string> seen;
for (const RecipeDef& recipe : m_config->recipes.recipes)
{
for (const RecipeIngredient& ing : recipe.inputs)
{
seen.insert(ing.item);
}
for (const RecipeOutput& out : recipe.outputs)
{
seen.insert(out.item);
}
}
return std::vector<std::string>(seen.begin(), seen.end());
}
void SelectedBuildingPanel::onClearBelt()
{
std::vector<QPoint> tiles;
for (EntityId id : m_selection)
{
const Building* b = m_sim->buildings().findBuilding(id);
if (b && isBeltLike(b->type))
{
for (const QPoint& cell : b->bodyCells)
{
tiles.push_back(cell);
}
}
}
if (!tiles.empty())
{
m_sim->belts().clearTiles(tiles);
}
}