353 lines
10 KiB
C++
353 lines
10 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 "BlueprintSerializer.h"
|
|
|
|
#include "Building.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)
|
|
, m_deleteMode(false)
|
|
, m_activeIndex(-1)
|
|
{
|
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(4, 4, 4, 4);
|
|
layout->setSpacing(4);
|
|
|
|
m_createBtn = new QPushButton("Create Blueprint", this);
|
|
m_createBtn->setFixedHeight(48);
|
|
m_createBtn->setEnabled(false);
|
|
layout->addWidget(m_createBtn);
|
|
|
|
m_deleteBtn = new QPushButton("Delete Blueprint", this);
|
|
m_deleteBtn->setFixedHeight(48);
|
|
m_deleteBtn->setCheckable(true);
|
|
layout->addWidget(m_deleteBtn);
|
|
|
|
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);
|
|
connect(m_deleteBtn, &QPushButton::clicked, this, &BlueprintPanel::onDeleteClicked);
|
|
|
|
QHBoxLayout* ioLayout = new QHBoxLayout();
|
|
m_saveBtn = new QPushButton("Save", this);
|
|
m_loadBtn = new QPushButton("Load", this);
|
|
m_saveBtn->setFixedHeight(36);
|
|
m_loadBtn->setFixedHeight(36);
|
|
ioLayout->addWidget(m_saveBtn);
|
|
ioLayout->addWidget(m_loadBtn);
|
|
layout->addLayout(ioLayout);
|
|
|
|
connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked);
|
|
connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked);
|
|
}
|
|
|
|
void BlueprintPanel::onSelectionChanged(const std::vector<EntityId>& ids)
|
|
{
|
|
m_selectedIds = ids;
|
|
refreshButtonStates();
|
|
}
|
|
|
|
void BlueprintPanel::onStateUpdated(Tick /*tick*/, int blocks, double /*speed*/)
|
|
{
|
|
m_currentBlocks = blocks;
|
|
refreshButtonStates();
|
|
}
|
|
|
|
void BlueprintPanel::clearActiveBlueprintButton()
|
|
{
|
|
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
|
|
{
|
|
m_blueprintButtons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false);
|
|
}
|
|
m_activeIndex = -1;
|
|
refreshButtonStates();
|
|
}
|
|
|
|
void BlueprintPanel::onCreateClicked()
|
|
{
|
|
if (m_selectedIds.empty()) { return; }
|
|
|
|
Blueprint bp = createBlueprintFromSelection();
|
|
if (bp.buildings.empty()) { return; }
|
|
|
|
bool ok = false;
|
|
const QString name = QInputDialog::getText(
|
|
this, "Create Blueprint", "Blueprint name:", QLineEdit::Normal, QString(), &ok);
|
|
if (!ok || name.trimmed().isEmpty()) { return; }
|
|
|
|
bp.name = name.trimmed();
|
|
m_blueprints.push_back(bp);
|
|
rebuildButtons();
|
|
}
|
|
|
|
void BlueprintPanel::onDeleteClicked()
|
|
{
|
|
if (!m_deleteMode)
|
|
{
|
|
m_deleteMode = true;
|
|
m_deleteBtn->setChecked(true);
|
|
if (m_activeIndex >= 0)
|
|
{
|
|
clearActiveBlueprintButton();
|
|
emit exitBlueprintModeRequested();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_deleteMode = false;
|
|
m_deleteBtn->setChecked(false);
|
|
}
|
|
}
|
|
|
|
void BlueprintPanel::onBlueprintButtonClicked(int index)
|
|
{
|
|
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
|
|
|
if (m_deleteMode)
|
|
{
|
|
m_blueprints.erase(m_blueprints.begin() + index);
|
|
m_deleteMode = false;
|
|
m_deleteBtn->setChecked(false);
|
|
m_activeIndex = -1;
|
|
rebuildButtons();
|
|
return;
|
|
}
|
|
|
|
if (m_activeIndex == index)
|
|
{
|
|
clearActiveBlueprintButton();
|
|
emit exitBlueprintModeRequested();
|
|
return;
|
|
}
|
|
|
|
if (m_activeIndex >= 0 && 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);
|
|
emit blueprintPlacementRequested(m_blueprints[static_cast<std::size_t>(index)]);
|
|
}
|
|
|
|
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
|
{
|
|
struct Entry
|
|
{
|
|
const Building* building;
|
|
};
|
|
std::vector<Entry> entries;
|
|
entries.reserve(m_selectedIds.size());
|
|
|
|
for (const EntityId id : m_selectedIds)
|
|
{
|
|
const Building* b = m_sim->buildings().findBuilding(id);
|
|
if (!b) { continue; }
|
|
const bool placeable = [&]() {
|
|
for (const BuildingDef& def : m_config->buildings.buildings)
|
|
{
|
|
if (def.type == b->type) { return def.playerPlaceable; }
|
|
}
|
|
return false;
|
|
}();
|
|
if (placeable) { entries.push_back({ b }); }
|
|
}
|
|
|
|
if (entries.empty()) { return Blueprint{}; }
|
|
|
|
int minX = INT_MAX, maxX = INT_MIN;
|
|
int minY = INT_MAX, maxY = INT_MIN;
|
|
for (const Entry& e : entries)
|
|
{
|
|
for (const QPoint& cell : e.building->bodyCells)
|
|
{
|
|
minX = std::min(minX, cell.x());
|
|
maxX = std::max(maxX, cell.x());
|
|
minY = std::min(minY, cell.y());
|
|
maxY = std::max(maxY, cell.y());
|
|
}
|
|
}
|
|
|
|
const QPoint center((minX + maxX) / 2, (minY + maxY) / 2);
|
|
|
|
Blueprint bp;
|
|
bp.buildings.reserve(entries.size());
|
|
for (const Entry& e : entries)
|
|
{
|
|
BlueprintBuilding bb;
|
|
bb.type = e.building->type;
|
|
bb.rotation = e.building->rotation;
|
|
bb.offset = e.building->anchor - center;
|
|
bb.recipeId = e.building->recipeId;
|
|
bp.buildings.push_back(bb);
|
|
}
|
|
return bp;
|
|
}
|
|
|
|
int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const
|
|
{
|
|
int total = 0;
|
|
for (const BlueprintBuilding& bb : bp.buildings)
|
|
{
|
|
for (const BuildingDef& def : m_config->buildings.buildings)
|
|
{
|
|
if (def.type == bb.type)
|
|
{
|
|
total += def.cost;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void BlueprintPanel::rebuildButtons()
|
|
{
|
|
for (QPushButton* btn : m_blueprintButtons)
|
|
{
|
|
m_buttonsLayout->removeWidget(btn);
|
|
delete btn;
|
|
}
|
|
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" + QString::number(cost) + " Blocks";
|
|
|
|
QPushButton* btn = new QPushButton(label, m_buttonsContainer);
|
|
btn->setCheckable(true);
|
|
btn->setFixedHeight(48);
|
|
m_buttonsLayout->insertWidget(i, btn);
|
|
|
|
const int capturedIndex = i;
|
|
connect(btn, &QPushButton::clicked, this, [this, capturedIndex]() {
|
|
onBlueprintButtonClicked(capturedIndex);
|
|
});
|
|
|
|
m_blueprintButtons.push_back(btn);
|
|
}
|
|
|
|
refreshButtonStates();
|
|
}
|
|
|
|
void BlueprintPanel::onSaveClicked()
|
|
{
|
|
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
|
try
|
|
{
|
|
const std::string content = BlueprintSerializer::serialize(m_blueprints);
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
|
{
|
|
QMessageBox::critical(this, "Save Failed",
|
|
QString("Could not open file for writing:\n%1").arg(path));
|
|
return;
|
|
}
|
|
file.write(QByteArray::fromStdString(content));
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
QMessageBox::critical(this, "Save Failed",
|
|
QString("Failed to save blueprints:\n%1").arg(e.what()));
|
|
}
|
|
}
|
|
|
|
void BlueprintPanel::onLoadClicked()
|
|
{
|
|
QMessageBox box(this);
|
|
box.setWindowTitle("Load Blueprints");
|
|
box.setText("Load blueprints? This will replace all current blueprints.");
|
|
QPushButton* confirmBtn = box.addButton("Confirm", QMessageBox::AcceptRole);
|
|
box.addButton("Cancel", QMessageBox::RejectRole);
|
|
box.exec();
|
|
if (box.clickedButton() != confirmBtn) { return; }
|
|
|
|
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
|
try
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
QMessageBox::critical(this, "Load Failed",
|
|
QString("Could not open file:\n%1").arg(path));
|
|
return;
|
|
}
|
|
const std::string content = file.readAll().toStdString();
|
|
std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(content);
|
|
|
|
if (m_activeIndex >= 0)
|
|
{
|
|
emit exitBlueprintModeRequested();
|
|
m_activeIndex = -1;
|
|
}
|
|
if (m_deleteMode)
|
|
{
|
|
m_deleteMode = false;
|
|
m_deleteBtn->setChecked(false);
|
|
}
|
|
|
|
m_blueprints = std::move(loaded);
|
|
rebuildButtons();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
QMessageBox::critical(this, "Load Failed",
|
|
QString("Failed to load blueprints:\n%1").arg(e.what()));
|
|
}
|
|
}
|
|
|
|
void BlueprintPanel::refreshButtonStates()
|
|
{
|
|
const bool anyPlaceable = [&]() {
|
|
for (const EntityId id : m_selectedIds)
|
|
{
|
|
const Building* b = m_sim->buildings().findBuilding(id);
|
|
if (!b) { continue; }
|
|
for (const BuildingDef& def : m_config->buildings.buildings)
|
|
{
|
|
if (def.type == b->type) { return def.playerPlaceable; }
|
|
}
|
|
}
|
|
return false;
|
|
}();
|
|
m_createBtn->setEnabled(anyPlaceable);
|
|
|
|
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);
|
|
}
|
|
}
|