Files
dota_factory/src/ui/BlueprintPanel.cpp

282 lines
8.7 KiB
C++

#include "BlueprintPanel.h"
#include <algorithm>
#include <climits>
#include <QCoreApplication>
#include <QFile>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QMessageBox>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#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)
, m_currentBlocks(0)
{
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<BuildingId>& ids)
{
m_selectedBuildingIds = ids;
refreshButtonStates();
}
void BlueprintPanel::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
{
m_currentBlocks = event->blocks;
refreshButtonStates();
}
void BlueprintPanel::clearActiveBlueprintButton()
{
if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
{
m_blueprintButtons[static_cast<std::size_t>(*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<ExitBlueprintModeRequestedEvent>());
}
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<int>(m_blueprints.size())) { return; }
if (m_activeIndex == index)
{
clearActiveBlueprintButton();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBlueprintModeRequestedEvent>());
return;
}
if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
{
m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
}
m_activeIndex = index;
m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true);
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintPlacementRequestedEvent>(m_blueprints[static_cast<std::size_t>(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<int>(m_blueprints.size()); ++i)
{
const Blueprint& bp = m_blueprints[static_cast<std::size_t>(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));
for (int i = 0; i < static_cast<int>(m_blueprintButtons.size()); ++i)
{
const int cost = computeBlueprintCost(m_blueprints[static_cast<std::size_t>(i)]);
const bool canAfford = m_currentBlocks >= cost;
m_blueprintButtons[static_cast<std::size_t>(i)]->setEnabled(
canAfford || m_activeIndex == i);
}
}
void BlueprintPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
{
onSelectionChanged(event->ids);
}
void BlueprintPanel::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
{
clearActiveBlueprintButton();
}
void BlueprintPanel::handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> /*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<BlueprintPlacementRequestedEvent>(std::move(bp)));
}