#include "BlueprintPanel.h" #include #include #include #include #include #include #include #include #include #include #include "BlueprintPlacementRequestedEvent.h" #include "BlueprintSerializer.h" #include "BuildingBlocksChangedEvent.h" #include "EventManager.h" #include "ExitBlueprintModeRequestedEvent.h" #include "Building.h" #include "BuildingConfig.h" #include "BuildingSystem.h" #include "Simulation.h" BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent) : QWidget(parent) , m_sim(sim) , m_config(config) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(4, 4, 4, 4); layout->setSpacing(4); m_createBtn = new QPushButton(tr("Create Blueprint"), this); m_createBtn->setFixedHeight(48); m_createBtn->setEnabled(false); layout->addWidget(m_createBtn); QScrollArea* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_buttonsContainer = new QWidget(scrollArea); m_buttonsLayout = new QVBoxLayout(m_buttonsContainer); m_buttonsLayout->setContentsMargins(0, 0, 0, 0); m_buttonsLayout->setSpacing(4); m_buttonsLayout->addStretch(); scrollArea->setWidget(m_buttonsContainer); layout->addWidget(scrollArea, 1); connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked); loadFromDisk(); rebuildButtons(); registerForEvents(); } BlueprintPanel::~BlueprintPanel() { saveToDisk(); unregisterForEvents(); } void BlueprintPanel::onSelectionChanged(const std::vector& ids) { m_selectedBuildingIds = ids; refreshButtonStates(); } void BlueprintPanel::handleEvent(std::shared_ptr /*event*/) { refreshButtonStates(); } void BlueprintPanel::clearActiveBlueprintButton() { if (m_activeIndex.has_value() && *m_activeIndex < static_cast(m_blueprintButtons.size())) { m_blueprintButtons[static_cast(*m_activeIndex)]->setChecked(false); } m_activeIndex = std::nullopt; refreshButtonStates(); } void BlueprintPanel::onCreateClicked() { if (m_selectedBuildingIds.empty()) { return; } Blueprint bp = createBlueprintFromSelection(); if (bp.buildings.empty()) { return; } bool ok = false; const QString name = QInputDialog::getText( this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal, QString(), &ok); if (!ok || name.trimmed().isEmpty()) { return; } bp.name = name.trimmed(); m_blueprints.push_back(bp); rebuildButtons(); } void BlueprintPanel::onDeleteBlueprintClicked(int index) { if (m_activeIndex == index) { m_activeIndex = std::nullopt; EventManager::getInstance()->sendEventImmediately( std::make_shared()); } else if (m_activeIndex.has_value() && *m_activeIndex > index) { --*m_activeIndex; } m_blueprints.erase(m_blueprints.begin() + index); rebuildButtons(); } void BlueprintPanel::onBlueprintButtonClicked(int index) { if (index < 0 || index >= static_cast(m_blueprints.size())) { return; } if (m_activeIndex == index) { clearActiveBlueprintButton(); EventManager::getInstance()->sendEventImmediately( std::make_shared()); return; } if (m_activeIndex.has_value() && *m_activeIndex < static_cast(m_blueprintButtons.size())) { m_blueprintButtons[static_cast(*m_activeIndex)]->setChecked(false); } m_activeIndex = index; m_blueprintButtons[static_cast(index)]->setChecked(true); EventManager::getInstance()->sendEventImmediately( std::make_shared(m_blueprints[static_cast(index)])); } Blueprint BlueprintPanel::createBlueprintFromSelection() const { // Capture is shared, testable logic in lib/sim: it resolves each selected id as an // operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE, // REQ-UI-BLUEPRINT-STORAGE). return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds); } int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const { int total = 0; for (const BlueprintBuilding& bb : bp.buildings) { const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type); if (def) { total += def->cost; } } return total; } void BlueprintPanel::rebuildButtons() { while (m_buttonsLayout->count() > 1) { QLayoutItem* item = m_buttonsLayout->takeAt(0); if (item->widget()) { delete item->widget(); } delete item; } m_blueprintButtons.clear(); for (int i = 0; i < static_cast(m_blueprints.size()); ++i) { const Blueprint& bp = m_blueprints[static_cast(i)]; const int cost = computeBlueprintCost(bp); const QString label = bp.name + "\n" + tr("%1 Building Blocks").arg(cost); QWidget* row = new QWidget(m_buttonsContainer); QHBoxLayout* rowLayout = new QHBoxLayout(row); rowLayout->setContentsMargins(0, 0, 0, 0); rowLayout->setSpacing(4); QPushButton* btn = new QPushButton(label, row); btn->setCheckable(true); btn->setFixedHeight(48); QPushButton* delBtn = new QPushButton("\xc3\x97", row); delBtn->setFixedWidth(28); delBtn->setFixedHeight(48); rowLayout->addWidget(btn, 1); rowLayout->addWidget(delBtn, 0); m_buttonsLayout->insertWidget(i, row); const int capturedIndex = i; connect(btn, &QPushButton::clicked, this, [this, capturedIndex]() { onBlueprintButtonClicked(capturedIndex); }); connect(delBtn, &QPushButton::clicked, this, [this, capturedIndex]() { onDeleteBlueprintClicked(capturedIndex); }); m_blueprintButtons.push_back(btn); } refreshButtonStates(); } void BlueprintPanel::saveToDisk() const { // Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE). const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } try { const std::string content = BlueprintSerializer::serialize(m_blueprints); file.write(QByteArray::fromStdString(content)); } catch (...) {} } void BlueprintPanel::loadFromDisk() { // Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error. const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } try { m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString()); } catch (const std::exception& e) { QMessageBox::critical(this, tr("Load Failed"), tr("Failed to load blueprints:\n%1").arg(e.what())); m_blueprints.clear(); } } void BlueprintPanel::refreshButtonStates() { // A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE). m_createBtn->setEnabled(selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds)); const int blocks = m_sim->getBuildingBlocksStock(); for (int i = 0; i < static_cast(m_blueprintButtons.size()); ++i) { const int cost = computeBlueprintCost(m_blueprints[static_cast(i)]); const bool canAfford = blocks >= cost; m_blueprintButtons[static_cast(i)]->setEnabled( canAfford || m_activeIndex == i); } } void BlueprintPanel::handleEvent(std::shared_ptr event) { onSelectionChanged(event->ids); } void BlueprintPanel::handleEvent(std::shared_ptr /*event*/) { clearActiveBlueprintButton(); } void BlueprintPanel::handleEvent(std::shared_ptr /*event*/) { // Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and // enter placement mode without adding it to the list or persisting it. If nothing // player-placeable is selected, do nothing. Blueprint bp = createBlueprintFromSelection(); if (bp.buildings.empty()) { return; } // No saved blueprint is active while a temporary one is being placed. clearActiveBlueprintButton(); EventManager::getInstance()->sendEventImmediately( std::make_shared(std::move(bp))); }