ship layout blueprints

This commit is contained in:
2026-05-19 21:01:10 +02:00
parent d08bf5d37b
commit d397b9969a
14 changed files with 511 additions and 7 deletions

View File

@@ -1,6 +1,8 @@
#include "MainWindow.h"
#include <QApplication>
#include <QCloseEvent>
#include <QFile>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
@@ -14,6 +16,7 @@
#include "GameWorldView.h"
#include "HeaderBar.h"
#include "SelectedBuildingPanel.h"
#include "ShipLayoutBlueprintSerializer.h"
#include "ShipLayoutDialog.h"
#include "Simulation.h"
#include "Tick.h"
@@ -111,6 +114,24 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
m_gameWorldView->setFocus();
}
});
// Load layout blueprints from disk. Missing file is silently ignored.
const QString bpPath = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
QFile bpFile(bpPath);
if (bpFile.open(QIODevice::ReadOnly | QIODevice::Text))
{
try
{
m_layoutBlueprints = ShipLayoutBlueprintSerializer::deserialize(
bpFile.readAll().toStdString());
}
catch (const std::exception& e)
{
QMessageBox::critical(this, "Load Error",
QString("Failed to load ship_layouts.toml:\n%1").arg(e.what()));
m_layoutBlueprints.clear();
}
}
}
void MainWindow::resizeEvent(QResizeEvent* event)
@@ -119,6 +140,23 @@ void MainWindow::resizeEvent(QResizeEvent* event)
layoutPanels();
}
void MainWindow::closeEvent(QCloseEvent* event)
{
const QString path = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
QFile file(path);
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
try
{
const std::string content =
ShipLayoutBlueprintSerializer::serialize(m_layoutBlueprints);
file.write(QByteArray::fromStdString(content));
}
catch (...) {}
}
QWidget::closeEvent(event);
}
void MainWindow::layoutPanels()
{
const int totalW = width();
@@ -199,7 +237,8 @@ void MainWindow::onLayoutDialogRequested(EntityId shipyardId)
currentLayout = *b->shipLayout;
}
ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout, this);
ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout,
m_layoutBlueprints, this);
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
{
m_sim->buildings().setShipLayout(shipyardId, *dialog.result());

View File

@@ -1,10 +1,12 @@
#pragma once
#include <string>
#include <vector>
#include <QWidget>
#include "EntityId.h"
#include "ShipLayoutBlueprint.h"
#include "Tick.h"
#include "VisualsConfig.h"
@@ -14,6 +16,7 @@ class HeaderBar;
class SelectedBuildingPanel;
class BuildButtonGrid;
class BlueprintPanel;
class QCloseEvent;
class QResizeEvent;
class MainWindow : public QWidget
@@ -25,6 +28,7 @@ public:
protected:
void resizeEvent(QResizeEvent* event) override;
void closeEvent(QCloseEvent* event) override;
private slots:
void onGameOver();
@@ -44,4 +48,6 @@ private:
BuildButtonGrid* m_buildButtonGrid;
BlueprintPanel* m_blueprintPanel;
QWidget* m_bottomPanel;
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
};

View File

@@ -1,13 +1,16 @@
#include "ShipLayoutDialog.h"
#include <cctype>
#include <functional>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QScrollArea>
#include <QSignalMapper>
#include <QVBoxLayout>
@@ -263,6 +266,118 @@ private:
};
// ---------------------------------------------------------------------------
// Blueprint panel (third column of the layout dialog)
// ---------------------------------------------------------------------------
class ShipLayoutBlueprintPanel : public QWidget
{
public:
ShipLayoutBlueprintPanel(
std::vector<ShipLayoutBlueprint>& allBlueprints,
const std::string& shipType,
std::function<std::vector<PlacedModule>()> getModules,
std::function<void(const std::vector<PlacedModule>&)> loadModules,
QWidget* parent = nullptr)
: QWidget(parent)
, m_allBlueprints(allBlueprints)
, m_shipType(shipType)
, m_getModules(std::move(getModules))
, m_loadModules(std::move(loadModules))
{
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 4, 4, 4);
layout->setSpacing(4);
QPushButton* createBtn = new QPushButton("Create Blueprint", this);
createBtn->setFixedHeight(36);
layout->addWidget(createBtn);
m_scrollArea = new QScrollArea(this);
m_scrollArea->setWidgetResizable(true);
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scrollContainer = new QWidget(m_scrollArea);
m_listLayout = new QVBoxLayout(m_scrollContainer);
m_listLayout->setContentsMargins(0, 0, 0, 0);
m_listLayout->setSpacing(2);
m_listLayout->addStretch();
m_scrollArea->setWidget(m_scrollContainer);
layout->addWidget(m_scrollArea, 1);
connect(createBtn, &QPushButton::clicked, this, [this]() {
bool ok = false;
const QString name = QInputDialog::getText(
this, "Create Blueprint", "Blueprint name:",
QLineEdit::Normal, QString(), &ok);
if (!ok || name.trimmed().isEmpty()) { return; }
ShipLayoutBlueprint bp;
bp.name = name.trimmed();
bp.shipType = m_shipType;
bp.modules = m_getModules();
m_allBlueprints.push_back(std::move(bp));
rebuildList();
});
rebuildList();
}
private:
void rebuildList()
{
// Remove all items except the trailing stretch.
while (m_listLayout->count() > 1)
{
QLayoutItem* item = m_listLayout->takeAt(0);
if (item->widget()) { delete item->widget(); }
delete item;
}
for (std::size_t i = 0; i < m_allBlueprints.size(); ++i)
{
const ShipLayoutBlueprint& bp = m_allBlueprints[i];
if (bp.shipType != m_shipType) { continue; }
QWidget* row = new QWidget(m_scrollContainer);
QHBoxLayout* rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
rowLayout->setSpacing(2);
QPushButton* nameBtn = new QPushButton(bp.name, row);
nameBtn->setFixedHeight(36);
QPushButton* delBtn = new QPushButton("\xc3\x97", row);
delBtn->setFixedWidth(28);
delBtn->setFixedHeight(36);
rowLayout->addWidget(nameBtn, 1);
rowLayout->addWidget(delBtn, 0);
m_listLayout->insertWidget(m_listLayout->count() - 1, row);
const std::vector<PlacedModule> modules = bp.modules;
connect(nameBtn, &QPushButton::clicked, this, [this, modules]() {
m_loadModules(modules);
});
connect(delBtn, &QPushButton::clicked, this, [this, i]() {
m_allBlueprints.erase(m_allBlueprints.begin()
+ static_cast<std::ptrdiff_t>(i));
rebuildList();
});
}
}
std::vector<ShipLayoutBlueprint>& m_allBlueprints;
std::string m_shipType;
std::function<std::vector<PlacedModule>()> m_getModules;
std::function<void(const std::vector<PlacedModule>&)> m_loadModules;
QScrollArea* m_scrollArea = nullptr;
QWidget* m_scrollContainer = nullptr;
QVBoxLayout* m_listLayout = nullptr;
};
// ---------------------------------------------------------------------------
// ShipLayoutDialog implementation
// ---------------------------------------------------------------------------
@@ -270,6 +385,7 @@ private:
ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
const std::string& shipId,
const ShipLayoutConfig& currentLayout,
std::vector<ShipLayoutBlueprint>& allBlueprints,
QWidget* parent)
: QDialog(parent)
, m_config(config)
@@ -408,6 +524,15 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
mainLayout->addLayout(rightLayout);
// Right: blueprint panel (third column).
ShipLayoutBlueprintPanel* bpPanel = new ShipLayoutBlueprintPanel(
allBlueprints,
m_shipId,
[this]() { return m_placedModules; },
[this](const std::vector<PlacedModule>& mods) { loadLayoutBlueprint(mods); },
this);
mainLayout->addWidget(bpPanel);
// Grid click handler.
connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) {
if (m_activeModuleIndex == -2)
@@ -614,3 +739,60 @@ std::vector<std::string> ShipLayoutDialog::rotatedMask(const ModuleDef& def,
}
return result;
}
void ShipLayoutDialog::loadLayoutBlueprint(const std::vector<PlacedModule>& modules)
{
m_placedModules.clear();
// Build a temporary occupancy grid to detect overlaps within the blueprint.
std::vector<std::vector<bool>> occupied(m_rows, std::vector<bool>(m_cols, false));
for (const PlacedModule& pm : modules)
{
// Validate module type exists.
const ModuleDef* def = nullptr;
for (const ModuleDef& d : m_config->modules.modules)
{
if (d.id == pm.moduleId) { def = &d; break; }
}
if (!def) { continue; }
const std::vector<std::string> mask = rotatedMask(*def, pm.rotation);
bool valid = true;
for (int mr = 0; mr < static_cast<int>(mask.size()) && valid; ++mr)
{
for (int mc = 0; mc < static_cast<int>(mask[mr].size()) && valid; ++mc)
{
if (mask[mr][mc] != 'O') { continue; }
const int gr = pm.position.y() + mr;
const int gc = pm.position.x() + mc;
if (gr < 0 || gr >= m_rows || gc < 0 || gc >= m_cols) { valid = false; break; }
if (!m_grid[gr][gc].buildable) { valid = false; break; }
if (occupied[gr][gc]) { valid = false; break; }
}
}
if (!valid) { continue; }
// Mark cells as occupied.
for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
{
for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
{
if (mask[mr][mc] != 'O') { continue; }
const int gr = pm.position.y() + mr;
const int gc = pm.position.x() + mc;
if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
{
occupied[gr][gc] = true;
}
}
}
m_placedModules.push_back(pm);
}
rebuildOccupancy();
updateGridWidget();
}

View File

@@ -10,6 +10,7 @@
#include "GameConfig.h"
#include "Rotation.h"
#include "ShipLayout.h"
#include "ShipLayoutBlueprint.h"
class QPushButton;
@@ -21,6 +22,7 @@ public:
ShipLayoutDialog(const GameConfig* config,
const std::string& shipId,
const ShipLayoutConfig& currentLayout,
std::vector<ShipLayoutBlueprint>& allBlueprints,
QWidget* parent = nullptr);
std::optional<ShipLayoutConfig> result() const;
@@ -49,6 +51,7 @@ private:
void updateGridWidget();
bool canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const;
std::vector<std::string> rotatedMask(const ModuleDef& def, Rotation rotation) const;
void loadLayoutBlueprint(const std::vector<PlacedModule>& modules);
const GameConfig* m_config;
std::string m_shipId;