#include "SelectedBuildingPanel.h" #include #include #include #include #include #include #include #include #include #include #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(std::toupper(static_cast(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(&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& 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( 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& 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 outCounts; for (const Item& item : b->outputBuffer.items) { outCounts[item.type.id]++; } bufText += "Output: "; for (const RecipeOutput& out : recipe->outputs) { const std::map::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 outCounts; for (const Item& item : b->outputBuffer.items) { outCounts[item.type.id]++; } bufText += "Output: "; for (const std::pair& 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( 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& 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 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& 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 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 items = allItemIds(); auto populateList = [&](QListWidget* list, QLabel* label, const QString& dirLabel, const std::vector& 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 { std::vector 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 SelectedBuildingPanel::allItemIds() const { std::set 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(seen.begin(), seen.end()); } void SelectedBuildingPanel::onClearBelt() { std::vector 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); } }