207 lines
7.0 KiB
C++
207 lines
7.0 KiB
C++
#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);
|
|
}
|
|
|
|
bool BlueprintLibrary::getHasTemporaryBlueprint() const
|
|
{
|
|
return m_temporaryBlueprint.has_value();
|
|
}
|
|
|
|
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*/)
|
|
{
|
|
// Only the saved-blueprint index is cleared. A temporary blueprint deliberately
|
|
// survives its placement mode so V can re-enter it (REQ-UI-BLUEPRINT-TEMP).
|
|
m_activeIndex = std::nullopt;
|
|
}
|
|
|
|
void BlueprintLibrary::handleEvent(
|
|
std::shared_ptr<const TemporaryBlueprintCaptureRequestedEvent> /*event*/)
|
|
{
|
|
// C (REQ-UI-BLUEPRINT-TEMP): capture the current selection and enter placement mode
|
|
// for it, without naming it, listing it, or persisting it. If nothing player-placeable
|
|
// is selected, do nothing at all -- in particular, keep the previous temporary
|
|
// blueprint, which V can still place.
|
|
Blueprint blueprint = createBlueprintFromSelection();
|
|
if (blueprint.buildings.empty()) { return; }
|
|
|
|
m_temporaryBlueprint = std::move(blueprint);
|
|
// No saved blueprint is active while a temporary one is being placed.
|
|
m_activeIndex = std::nullopt;
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<BlueprintPlacementRequestedEvent>(*m_temporaryBlueprint));
|
|
}
|
|
|
|
void BlueprintLibrary::handleEvent(
|
|
std::shared_ptr<const TemporaryBlueprintPlaceRequestedEvent> /*event*/)
|
|
{
|
|
// V (REQ-UI-BLUEPRINT-TEMP): re-enter placement mode for the temporary blueprint,
|
|
// capturing nothing. With none captured, nothing happens -- no mode is entered and
|
|
// whichever mode is active is left alone. Affordability is not checked here, matching
|
|
// C; cost is enforced at placement (REQ-UI-BLUEPRINT-PLACE). The blueprint is copied,
|
|
// not moved: it stays available for the next V.
|
|
if (!m_temporaryBlueprint.has_value()) { return; }
|
|
|
|
m_activeIndex = std::nullopt;
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<BlueprintPlacementRequestedEvent>(*m_temporaryBlueprint));
|
|
}
|
|
|
|
void BlueprintLibrary::handleEvent(std::shared_ptr<const GameResetEvent> /*event*/)
|
|
{
|
|
// A restart begins a new run, and the temporary blueprint belongs to the old one
|
|
// (REQ-UI-BLUEPRINT-TEMP). The saved blueprints are not run state and stay.
|
|
m_temporaryBlueprint = std::nullopt;
|
|
}
|
|
|
|
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 (...) {}
|
|
}
|