move blueprints out of the sidebar into Ctrl+C / Ctrl+V dialogs

This commit is contained in:
2026-08-06 19:03:59 +02:00
parent 2cbcf1554f
commit 18cfe238f6
22 changed files with 1021 additions and 400 deletions

175
src/ui/BlueprintLibrary.cpp Normal file
View File

@@ -0,0 +1,175 @@
#include "BlueprintLibrary.h"
#include <cstddef>
#include <utility>
#include <QCoreApplication>
#include <QFile>
#include <QMessageBox>
#include <QObject>
#include <QStringList>
#include "BlueprintPlacementRequestedEvent.h"
#include "BlueprintSerializer.h"
#include "EventManager.h"
#include "ExitBlueprintModeRequestedEvent.h"
#include "BuildingConfig.h"
#include "Simulation.h"
BlueprintLibrary::BlueprintLibrary(Simulation* sim, const GameConfig* config,
QWidget* dialogParent)
: m_sim(sim)
, m_config(config)
, m_dialogParent(dialogParent)
{
loadFromDisk();
registerForEvents();
}
BlueprintLibrary::~BlueprintLibrary()
{
saveToDisk();
unregisterForEvents();
}
bool BlueprintLibrary::getCanCaptureSelection() const
{
// A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE).
return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds);
}
void BlueprintLibrary::saveSelectionAs(const QString& name)
{
Blueprint blueprint = createBlueprintFromSelection();
if (blueprint.buildings.empty()) { return; }
blueprint.name = name;
m_blueprints.push_back(std::move(blueprint));
}
const std::vector<Blueprint>& BlueprintLibrary::getBlueprints() const
{
return m_blueprints;
}
QString BlueprintLibrary::getContentsSummary(int index) const
{
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return QString(); }
QStringList entries;
for (const BlueprintContentEntry& entry : summarizeBlueprintContents(
m_blueprints[static_cast<std::size_t>(index)], m_config->buildings))
{
// The same "<type> x <count>" notation the building multi-selection summary
// uses (REQ-UI-MULTI-SELECTION), which also sidesteps plural forms.
entries << QObject::tr("%1 x %2")
.arg(QString::fromStdString(entry.buildingName))
.arg(entry.count);
}
return entries.join(QStringLiteral(", "));
}
int BlueprintLibrary::getCost(int index) const
{
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return 0; }
return computeBlueprintCost(m_blueprints[static_cast<std::size_t>(index)],
m_config->buildings);
}
bool BlueprintLibrary::getCanAfford(int index) const
{
return m_sim->getBuildingBlocksStock() >= getCost(index);
}
void BlueprintLibrary::remove(int index)
{
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
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);
}
void BlueprintLibrary::beginPlacement(int index)
{
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
m_activeIndex = index;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintPlacementRequestedEvent>(
m_blueprints[static_cast<std::size_t>(index)]));
}
void BlueprintLibrary::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
{
m_selectedBuildingIds = event->ids;
}
void BlueprintLibrary::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
{
m_activeIndex = std::nullopt;
}
void BlueprintLibrary::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 blueprint = createBlueprintFromSelection();
if (blueprint.buildings.empty()) { return; }
// No saved blueprint is active while a temporary one is being placed.
m_activeIndex = std::nullopt;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintPlacementRequestedEvent>(std::move(blueprint)));
}
Blueprint BlueprintLibrary::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);
}
void BlueprintLibrary::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(m_dialogParent, QObject::tr("Load Failed"),
QObject::tr("Failed to load blueprints:\n%1").arg(e.what()));
m_blueprints.clear();
}
}
void BlueprintLibrary::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 (...) {}
}