implement ship modules
This commit is contained in:
@@ -8,6 +8,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -20,5 +22,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
|
||||
#include "BlueprintPanel.h"
|
||||
#include "BuildButtonGrid.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "GameWorldView.h"
|
||||
#include "HeaderBar.h"
|
||||
#include "SelectedBuildingPanel.h"
|
||||
#include "ShipLayoutDialog.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsLoader.h"
|
||||
@@ -62,6 +64,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
|
||||
connect(m_gameWorldView, &GameWorldView::escapeMenuRequested,
|
||||
this, &MainWindow::onEscapeMenuRequested);
|
||||
|
||||
connect(m_selectedBuildingPanel, &SelectedBuildingPanel::layoutDialogRequested,
|
||||
this, &MainWindow::onLayoutDialogRequested);
|
||||
|
||||
// Signals: build grid → game world
|
||||
connect(m_buildButtonGrid, &BuildButtonGrid::buildingTypeSelected,
|
||||
m_gameWorldView, &GameWorldView::enterBuilderMode);
|
||||
@@ -176,6 +181,33 @@ void MainWindow::onEscapeMenuRequested()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onLayoutDialogRequested(EntityId shipyardId)
|
||||
{
|
||||
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||
m_gameWorldView->setGameSpeed(0.0);
|
||||
|
||||
const Building* b = m_sim->buildings().findBuilding(shipyardId);
|
||||
if (!b)
|
||||
{
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
return;
|
||||
}
|
||||
|
||||
ShipLayoutConfig currentLayout;
|
||||
if (b->shipLayout.has_value())
|
||||
{
|
||||
currentLayout = *b->shipLayout;
|
||||
}
|
||||
|
||||
ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout, this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||
{
|
||||
m_sim->buildings().setShipLayout(shipyardId, *dialog.result());
|
||||
}
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
}
|
||||
|
||||
void MainWindow::onGameOver()
|
||||
{
|
||||
const Tick tick = m_sim->currentTick();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
@@ -29,6 +30,7 @@ private slots:
|
||||
void onGameOver();
|
||||
void onStateUpdated(Tick tick, int blocks, double speed);
|
||||
void onEscapeMenuRequested();
|
||||
void onLayoutDialogRequested(EntityId shipyardId);
|
||||
|
||||
private:
|
||||
void layoutPanels();
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ItemType.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayoutPreview.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
namespace
|
||||
@@ -99,6 +101,8 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
||||
m_filterAList = new QListWidget(this);
|
||||
m_filterBLabel = new QLabel(this);
|
||||
m_filterBList = new QListWidget(this);
|
||||
m_layoutPreview = new ShipLayoutPreview(this);
|
||||
m_configureLayoutBtn = new QPushButton("Configure Layout", this);
|
||||
m_buffersLabel = new QLabel(this);
|
||||
m_buffersLabel->setWordWrap(true);
|
||||
|
||||
@@ -107,6 +111,8 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
||||
|
||||
m_layout->addWidget(m_titleLabel);
|
||||
m_layout->addWidget(m_recipeCombo);
|
||||
m_layout->addWidget(m_layoutPreview);
|
||||
m_layout->addWidget(m_configureLayoutBtn);
|
||||
m_layout->addWidget(m_clearBeltBtn);
|
||||
m_layout->addWidget(m_filterALabel);
|
||||
m_layout->addWidget(m_filterAList);
|
||||
@@ -118,6 +124,12 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
||||
this, &SelectedBuildingPanel::onRecipeChanged);
|
||||
connect(m_clearBeltBtn, &QPushButton::clicked,
|
||||
this, &SelectedBuildingPanel::onClearBelt);
|
||||
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
|
||||
if (m_singleId != kInvalidEntityId)
|
||||
{
|
||||
emit layoutDialogRequested(m_singleId);
|
||||
}
|
||||
});
|
||||
connect(m_filterAList, &QListWidget::itemChanged,
|
||||
this, &SelectedBuildingPanel::onSplitterFilterChanged);
|
||||
connect(m_filterBList, &QListWidget::itemChanged,
|
||||
@@ -153,6 +165,8 @@ void SelectedBuildingPanel::buildEmpty()
|
||||
m_singleId = kInvalidEntityId;
|
||||
m_titleLabel->hide();
|
||||
m_recipeCombo->hide();
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
m_clearBeltBtn->hide();
|
||||
m_filterALabel->hide();
|
||||
m_filterAList->hide();
|
||||
@@ -248,10 +262,39 @@ void SelectedBuildingPanel::buildSingle(EntityId id)
|
||||
m_recipeCombo->setCurrentIndex(currentIdx >= 0 ? currentIdx : 0);
|
||||
m_recipeCombo->blockSignals(false);
|
||||
m_recipeCombo->show();
|
||||
|
||||
if (b->type == BuildingType::Shipyard && !b->recipeId.empty())
|
||||
{
|
||||
const ShipDef* sDef = findShipDef(b->recipeId);
|
||||
if (sDef && !sDef->layout.empty())
|
||||
{
|
||||
ShipLayoutConfig layout;
|
||||
if (b->shipLayout.has_value())
|
||||
{
|
||||
layout = *b->shipLayout;
|
||||
}
|
||||
m_layoutPreview->setShipAndLayout(
|
||||
sDef->layout, layout, &m_config->modules.modules);
|
||||
m_layoutPreview->show();
|
||||
m_configureLayoutBtn->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_recipeCombo->hide();
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
}
|
||||
|
||||
if (isBeltLike(b->type))
|
||||
@@ -307,6 +350,26 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
{
|
||||
if (mat.item == entry.first.id) { perCycle = mat.amount; break; }
|
||||
}
|
||||
if (b->shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||
{
|
||||
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||
{
|
||||
if (modDef.id == pm.moduleId)
|
||||
{
|
||||
for (const RecipeIngredient& ing : modDef.materials)
|
||||
{
|
||||
if (ing.item == entry.first.id)
|
||||
{
|
||||
perCycle += ing.amount;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bufText += QString::fromStdString(entry.first.id)
|
||||
+ ": " + QString::number(entry.second);
|
||||
@@ -354,10 +417,25 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
|
||||
if (isProductionBuilding(b->type) && (recipe || shipDef))
|
||||
{
|
||||
const double durationSeconds = recipe
|
||||
double durationSeconds = recipe
|
||||
? recipe->durationSeconds
|
||||
: shipDef->schematic.productionTimeSeconds;
|
||||
|
||||
if (shipDef && b->shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||
{
|
||||
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||
{
|
||||
if (modDef.id == pm.moduleId)
|
||||
{
|
||||
durationSeconds += modDef.productionTimeSeconds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bufText += QString("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
||||
|
||||
if (b->production.has_value())
|
||||
@@ -377,6 +455,17 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
}
|
||||
|
||||
m_buffersLabel->setText(bufText);
|
||||
|
||||
if (b->type == BuildingType::Shipyard && shipDef && !shipDef->layout.empty())
|
||||
{
|
||||
ShipLayoutConfig layout;
|
||||
if (b->shipLayout.has_value())
|
||||
{
|
||||
layout = *b->shipLayout;
|
||||
}
|
||||
m_layoutPreview->setShipAndLayout(
|
||||
shipDef->layout, layout, &m_config->modules.modules);
|
||||
}
|
||||
}
|
||||
|
||||
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
|
||||
@@ -481,6 +570,7 @@ void SelectedBuildingPanel::onRecipeChanged(int comboIndex)
|
||||
}
|
||||
const QString recipeId = m_recipeCombo->itemData(comboIndex).toString();
|
||||
m_sim->buildings().setRecipe(m_singleId, recipeId.toStdString());
|
||||
rebuild();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::buildSplitterFilters(QPoint splitterTile)
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
#include "EntityId.h"
|
||||
#include "GameConfig.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "Tick.h"
|
||||
|
||||
class Simulation;
|
||||
class ShipLayoutPreview;
|
||||
class QLabel;
|
||||
class QComboBox;
|
||||
class QListWidget;
|
||||
@@ -28,6 +30,9 @@ public:
|
||||
SelectedBuildingPanel(Simulation* sim, const GameConfig* config,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void layoutDialogRequested(EntityId shipyardId);
|
||||
|
||||
public slots:
|
||||
void onSelectionChanged(const std::vector<EntityId>& ids);
|
||||
void onStateUpdated(Tick tick, int blocks, double speed);
|
||||
@@ -63,6 +68,9 @@ private:
|
||||
QListWidget* m_filterBList;
|
||||
QLabel* m_buffersLabel;
|
||||
|
||||
ShipLayoutPreview* m_layoutPreview;
|
||||
QPushButton* m_configureLayoutBtn;
|
||||
|
||||
EntityId m_singleId;
|
||||
QPoint m_splitterTile;
|
||||
std::string m_currentRecipeId;
|
||||
|
||||
616
src/ui/ShipLayoutDialog.cpp
Normal file
616
src/ui/ShipLayoutDialog.cpp
Normal file
@@ -0,0 +1,616 @@
|
||||
#include "ShipLayoutDialog.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QSignalMapper>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const int kCellSize = 32;
|
||||
|
||||
QString displayName(const std::string& id)
|
||||
{
|
||||
QString result;
|
||||
bool nextUpper = true;
|
||||
for (char c : id)
|
||||
{
|
||||
if (c == '_')
|
||||
{
|
||||
result += ' ';
|
||||
nextUpper = true;
|
||||
}
|
||||
else if (nextUpper)
|
||||
{
|
||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
nextUpper = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
|
||||
{
|
||||
if (grid.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
const int srcH = static_cast<int>(grid.size());
|
||||
int srcW = 0;
|
||||
for (const std::string& row : grid)
|
||||
{
|
||||
const int w = static_cast<int>(row.size());
|
||||
if (w > srcW)
|
||||
{
|
||||
srcW = w;
|
||||
}
|
||||
}
|
||||
const int dstW = srcH;
|
||||
const int dstH = srcW;
|
||||
std::vector<std::string> dst(dstH, std::string(dstW, 'X'));
|
||||
for (int row = 0; row < srcH; ++row)
|
||||
{
|
||||
for (int col = 0; col < srcW; ++col)
|
||||
{
|
||||
const char ch = (col < static_cast<int>(grid[row].size()))
|
||||
? grid[row][col]
|
||||
: 'X';
|
||||
dst[col][srcH - 1 - row] = ch;
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grid rendering widget (nested inside dialog)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LayoutGridWidget : public QWidget
|
||||
{
|
||||
public:
|
||||
LayoutGridWidget(ShipLayoutDialog* dialog, QWidget* parent = nullptr)
|
||||
: QWidget(parent)
|
||||
, m_dialog(dialog)
|
||||
{
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void setGridData(const std::vector<std::vector<ShipLayoutDialog::CellInfo>>* grid,
|
||||
int rows, int cols,
|
||||
const std::vector<PlacedModule>* placed,
|
||||
const GameConfig* config)
|
||||
{
|
||||
m_grid = grid;
|
||||
m_rows = rows;
|
||||
m_cols = cols;
|
||||
m_placed = placed;
|
||||
m_config = config;
|
||||
setFixedSize(cols * kCellSize + 1, rows * kCellSize + 1);
|
||||
}
|
||||
|
||||
void setGhostData(int moduleIndex, Rotation rotation)
|
||||
{
|
||||
m_ghostModuleIdx = moduleIndex;
|
||||
m_ghostRotation = rotation;
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* /*event*/) override
|
||||
{
|
||||
if (!m_grid || m_rows == 0 || m_cols == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
for (int r = 0; r < m_rows; ++r)
|
||||
{
|
||||
for (int c = 0; c < m_cols; ++c)
|
||||
{
|
||||
const QRect cellRect(c * kCellSize, r * kCellSize, kCellSize, kCellSize);
|
||||
const ShipLayoutDialog::CellInfo& cell = (*m_grid)[r][c];
|
||||
|
||||
if (!cell.buildable)
|
||||
{
|
||||
painter.fillRect(cellRect, QColor(30, 30, 30));
|
||||
}
|
||||
else if (cell.moduleIndex >= 0)
|
||||
{
|
||||
const PlacedModule& pm = (*m_placed)[cell.moduleIndex];
|
||||
const ModuleDef* def = findModule(pm.moduleId);
|
||||
QColor color(Qt::gray);
|
||||
QString glyph;
|
||||
if (def)
|
||||
{
|
||||
color = QColor(QString::fromStdString(def->fillColor));
|
||||
glyph = QString::fromStdString(def->glyph);
|
||||
}
|
||||
painter.fillRect(cellRect, color);
|
||||
if (!glyph.isEmpty())
|
||||
{
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(cellRect, Qt::AlignCenter, glyph);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.fillRect(cellRect, QColor(240, 240, 240));
|
||||
}
|
||||
|
||||
painter.setPen(QColor(100, 100, 100));
|
||||
painter.drawRect(cellRect);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw ghost
|
||||
if (m_ghostModuleIdx >= 0 && m_hoverCell.x() >= 0 && m_config)
|
||||
{
|
||||
const ModuleDef& def = m_config->modules.modules[m_ghostModuleIdx];
|
||||
const std::vector<std::string> mask = rotateMask(def.surfaceMask, m_ghostRotation);
|
||||
QColor ghostColor(QString::fromStdString(def.fillColor));
|
||||
ghostColor.setAlpha(100);
|
||||
|
||||
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 = m_hoverCell.y() + mr;
|
||||
const int gc = m_hoverCell.x() + mc;
|
||||
if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
|
||||
{
|
||||
const QRect cellRect(gc * kCellSize, gr * kCellSize,
|
||||
kCellSize, kCellSize);
|
||||
painter.fillRect(cellRect, ghostColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* event) override
|
||||
{
|
||||
const QPoint pos = event->pos();
|
||||
const QPoint cell(pos.x() / kCellSize, pos.y() / kCellSize);
|
||||
if (cell != m_hoverCell)
|
||||
{
|
||||
m_hoverCell = cell;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void mousePressEvent(QMouseEvent* event) override
|
||||
{
|
||||
if (event->button() != Qt::LeftButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const QPoint pos = event->pos();
|
||||
const QPoint cell(pos.x() / kCellSize, pos.y() / kCellSize);
|
||||
emit m_dialog->gridCellClicked(cell);
|
||||
}
|
||||
|
||||
void leaveEvent(QEvent* /*event*/) override
|
||||
{
|
||||
m_hoverCell = QPoint(-1, -1);
|
||||
update();
|
||||
}
|
||||
|
||||
private:
|
||||
const ModuleDef* findModule(const std::string& id) const
|
||||
{
|
||||
if (!m_config)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (const ModuleDef& def : m_config->modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> rotateMask(const std::vector<std::string>& mask,
|
||||
Rotation rotation) const
|
||||
{
|
||||
int steps = 0;
|
||||
switch (rotation)
|
||||
{
|
||||
case Rotation::East: steps = 0; break;
|
||||
case Rotation::South: steps = 1; break;
|
||||
case Rotation::West: steps = 2; break;
|
||||
case Rotation::North: steps = 3; break;
|
||||
}
|
||||
std::vector<std::string> result = mask;
|
||||
for (int i = 0; i < steps; ++i)
|
||||
{
|
||||
result = rotateMaskCW(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ShipLayoutDialog* m_dialog;
|
||||
const std::vector<std::vector<ShipLayoutDialog::CellInfo>>* m_grid = nullptr;
|
||||
int m_rows = 0;
|
||||
int m_cols = 0;
|
||||
const std::vector<PlacedModule>* m_placed = nullptr;
|
||||
const GameConfig* m_config = nullptr;
|
||||
int m_ghostModuleIdx = -2;
|
||||
Rotation m_ghostRotation = Rotation::East;
|
||||
QPoint m_hoverCell = QPoint(-1, -1);
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ShipLayoutDialog implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
const std::string& shipId,
|
||||
const ShipLayoutConfig& currentLayout,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_config(config)
|
||||
, m_shipId(shipId)
|
||||
, m_rows(0)
|
||||
, m_cols(0)
|
||||
, m_placedModules(currentLayout.placedModules)
|
||||
, m_activeModuleIndex(-2)
|
||||
, m_currentRotation(Rotation::East)
|
||||
, m_removeButton(nullptr)
|
||||
, m_gridWidget(nullptr)
|
||||
{
|
||||
setWindowTitle("Configure Ship Layout");
|
||||
setModal(true);
|
||||
|
||||
// Find the ship's layout grid.
|
||||
for (const ShipDef& def : config->ships.ships)
|
||||
{
|
||||
if (def.id == shipId)
|
||||
{
|
||||
m_shipLayout = def.layout;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_rows = static_cast<int>(m_shipLayout.size());
|
||||
m_cols = 0;
|
||||
for (const std::string& row : m_shipLayout)
|
||||
{
|
||||
const int w = static_cast<int>(row.size());
|
||||
if (w > m_cols)
|
||||
{
|
||||
m_cols = w;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize grid.
|
||||
m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, -1}));
|
||||
for (int r = 0; r < m_rows; ++r)
|
||||
{
|
||||
for (int c = 0; c < static_cast<int>(m_shipLayout[r].size()); ++c)
|
||||
{
|
||||
if (m_shipLayout[r][c] == 'O')
|
||||
{
|
||||
m_grid[r][c].buildable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
rebuildOccupancy();
|
||||
|
||||
// --- UI layout ---
|
||||
QHBoxLayout* mainLayout = new QHBoxLayout(this);
|
||||
|
||||
// Left: grid widget.
|
||||
LayoutGridWidget* gridW = new LayoutGridWidget(this, this);
|
||||
gridW->setGridData(&m_grid, m_rows, m_cols, &m_placedModules, m_config);
|
||||
gridW->setGhostData(m_activeModuleIndex, m_currentRotation);
|
||||
m_gridWidget = gridW;
|
||||
mainLayout->addWidget(m_gridWidget);
|
||||
|
||||
// Right: module buttons + confirm/cancel.
|
||||
QVBoxLayout* rightLayout = new QVBoxLayout();
|
||||
|
||||
QGridLayout* buttonGrid = new QGridLayout();
|
||||
buttonGrid->setSpacing(4);
|
||||
|
||||
QSignalMapper* mapper = new QSignalMapper(this);
|
||||
int col = 0;
|
||||
int row = 0;
|
||||
const int kCols = 2;
|
||||
|
||||
for (int i = 0; i < static_cast<int>(config->modules.modules.size()); ++i)
|
||||
{
|
||||
const ModuleDef& def = config->modules.modules[i];
|
||||
const QString label = displayName(def.id)
|
||||
+ "\n" + QString::fromStdString(def.glyph);
|
||||
QPushButton* btn = new QPushButton(label, this);
|
||||
btn->setCheckable(true);
|
||||
btn->setFixedHeight(48);
|
||||
buttonGrid->addWidget(btn, row, col);
|
||||
m_moduleButtons.push_back(btn);
|
||||
|
||||
mapper->setMapping(btn, i);
|
||||
connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));
|
||||
|
||||
++col;
|
||||
if (col >= kCols)
|
||||
{
|
||||
col = 0;
|
||||
++row;
|
||||
}
|
||||
}
|
||||
connect(mapper, qOverload<int>(&QSignalMapper::mapped),
|
||||
this, &ShipLayoutDialog::onModuleButtonClicked);
|
||||
|
||||
// Remove button.
|
||||
m_removeButton = new QPushButton("Remove", this);
|
||||
m_removeButton->setCheckable(true);
|
||||
m_removeButton->setFixedHeight(48);
|
||||
if (col > 0)
|
||||
{
|
||||
++row;
|
||||
}
|
||||
buttonGrid->addWidget(m_removeButton, row, 0, 1, kCols);
|
||||
connect(m_removeButton, &QPushButton::clicked, this, [this]() {
|
||||
if (m_activeModuleIndex == -1)
|
||||
{
|
||||
m_activeModuleIndex = -2;
|
||||
m_removeButton->setChecked(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (QPushButton* btn : m_moduleButtons)
|
||||
{
|
||||
btn->setChecked(false);
|
||||
}
|
||||
m_activeModuleIndex = -1;
|
||||
m_removeButton->setChecked(true);
|
||||
}
|
||||
updateGridWidget();
|
||||
});
|
||||
|
||||
rightLayout->addLayout(buttonGrid);
|
||||
rightLayout->addStretch();
|
||||
|
||||
// Confirm / Cancel buttons.
|
||||
QHBoxLayout* bottomBar = new QHBoxLayout();
|
||||
QPushButton* confirmBtn = new QPushButton("Confirm", this);
|
||||
QPushButton* cancelBtn = new QPushButton("Cancel", this);
|
||||
bottomBar->addWidget(confirmBtn);
|
||||
bottomBar->addWidget(cancelBtn);
|
||||
rightLayout->addLayout(bottomBar);
|
||||
|
||||
connect(confirmBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onConfirm);
|
||||
connect(cancelBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onCancel);
|
||||
|
||||
mainLayout->addLayout(rightLayout);
|
||||
|
||||
// Grid click handler.
|
||||
connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) {
|
||||
if (m_activeModuleIndex == -2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_activeModuleIndex == -1)
|
||||
{
|
||||
// Remove mode: find and remove module at cell.
|
||||
if (cell.y() >= 0 && cell.y() < m_rows && cell.x() >= 0 && cell.x() < m_cols)
|
||||
{
|
||||
const int idx = m_grid[cell.y()][cell.x()].moduleIndex;
|
||||
if (idx >= 0)
|
||||
{
|
||||
m_placedModules.erase(m_placedModules.begin() + idx);
|
||||
rebuildOccupancy();
|
||||
updateGridWidget();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Place module.
|
||||
const ModuleDef& def = m_config->modules.modules[m_activeModuleIndex];
|
||||
if (canPlaceModule(def, cell, m_currentRotation))
|
||||
{
|
||||
PlacedModule pm;
|
||||
pm.moduleId = def.id;
|
||||
pm.position = cell;
|
||||
pm.rotation = m_currentRotation;
|
||||
m_placedModules.push_back(pm);
|
||||
rebuildOccupancy();
|
||||
updateGridWidget();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<ShipLayoutConfig> ShipLayoutDialog::result() const
|
||||
{
|
||||
return m_result;
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Q)
|
||||
{
|
||||
// Rotate CCW = 3 CW steps.
|
||||
switch (m_currentRotation)
|
||||
{
|
||||
case Rotation::East: m_currentRotation = Rotation::North; break;
|
||||
case Rotation::North: m_currentRotation = Rotation::West; break;
|
||||
case Rotation::West: m_currentRotation = Rotation::South; break;
|
||||
case Rotation::South: m_currentRotation = Rotation::East; break;
|
||||
}
|
||||
updateGridWidget();
|
||||
}
|
||||
else if (event->key() == Qt::Key_E)
|
||||
{
|
||||
// Rotate CW.
|
||||
switch (m_currentRotation)
|
||||
{
|
||||
case Rotation::East: m_currentRotation = Rotation::South; break;
|
||||
case Rotation::South: m_currentRotation = Rotation::West; break;
|
||||
case Rotation::West: m_currentRotation = Rotation::North; break;
|
||||
case Rotation::North: m_currentRotation = Rotation::East; break;
|
||||
}
|
||||
updateGridWidget();
|
||||
}
|
||||
else
|
||||
{
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::onModuleButtonClicked(int index)
|
||||
{
|
||||
if (m_activeModuleIndex == index)
|
||||
{
|
||||
m_moduleButtons[index]->setChecked(false);
|
||||
m_activeModuleIndex = -2;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(m_moduleButtons.size()); ++i)
|
||||
{
|
||||
m_moduleButtons[i]->setChecked(i == index);
|
||||
}
|
||||
m_removeButton->setChecked(false);
|
||||
m_activeModuleIndex = index;
|
||||
}
|
||||
updateGridWidget();
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::onConfirm()
|
||||
{
|
||||
ShipLayoutConfig layout;
|
||||
layout.placedModules = m_placedModules;
|
||||
m_result = layout;
|
||||
accept();
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::onCancel()
|
||||
{
|
||||
m_result = std::nullopt;
|
||||
reject();
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::rebuildOccupancy()
|
||||
{
|
||||
for (int r = 0; r < m_rows; ++r)
|
||||
{
|
||||
for (int c = 0; c < m_cols; ++c)
|
||||
{
|
||||
m_grid[r][c].moduleIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_placedModules.size()); ++i)
|
||||
{
|
||||
const PlacedModule& pm = m_placedModules[i];
|
||||
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);
|
||||
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)
|
||||
{
|
||||
m_grid[gr][gc].moduleIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::updateGridWidget()
|
||||
{
|
||||
LayoutGridWidget* gridW = static_cast<LayoutGridWidget*>(m_gridWidget);
|
||||
gridW->setGhostData(m_activeModuleIndex, m_currentRotation);
|
||||
gridW->update();
|
||||
}
|
||||
|
||||
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
|
||||
Rotation rotation) const
|
||||
{
|
||||
const std::vector<std::string> mask = rotatedMask(def, rotation);
|
||||
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 = position.y() + mr;
|
||||
const int gc = position.x() + mc;
|
||||
if (gr < 0 || gr >= m_rows || gc < 0 || gc >= m_cols)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!m_grid[gr][gc].buildable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_grid[gr][gc].moduleIndex >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> ShipLayoutDialog::rotatedMask(const ModuleDef& def,
|
||||
Rotation rotation) const
|
||||
{
|
||||
int steps = 0;
|
||||
switch (rotation)
|
||||
{
|
||||
case Rotation::East: steps = 0; break;
|
||||
case Rotation::South: steps = 1; break;
|
||||
case Rotation::West: steps = 2; break;
|
||||
case Rotation::North: steps = 3; break;
|
||||
}
|
||||
std::vector<std::string> result = def.surfaceMask;
|
||||
for (int i = 0; i < steps; ++i)
|
||||
{
|
||||
result = rotateMaskCW(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
70
src/ui/ShipLayoutDialog.h
Normal file
70
src/ui/ShipLayoutDialog.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPoint>
|
||||
|
||||
#include "GameConfig.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
class QPushButton;
|
||||
|
||||
class ShipLayoutDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ShipLayoutDialog(const GameConfig* config,
|
||||
const std::string& shipId,
|
||||
const ShipLayoutConfig& currentLayout,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
std::optional<ShipLayoutConfig> result() const;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
|
||||
signals:
|
||||
void gridCellClicked(QPoint cell);
|
||||
|
||||
private slots:
|
||||
void onModuleButtonClicked(int index);
|
||||
void onConfirm();
|
||||
void onCancel();
|
||||
|
||||
public:
|
||||
struct CellInfo
|
||||
{
|
||||
bool buildable;
|
||||
int moduleIndex; // -1 if empty
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
void rebuildOccupancy();
|
||||
void updateGridWidget();
|
||||
bool canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const;
|
||||
std::vector<std::string> rotatedMask(const ModuleDef& def, Rotation rotation) const;
|
||||
|
||||
const GameConfig* m_config;
|
||||
std::string m_shipId;
|
||||
std::vector<std::string> m_shipLayout;
|
||||
int m_rows;
|
||||
int m_cols;
|
||||
|
||||
std::vector<PlacedModule> m_placedModules;
|
||||
std::vector<std::vector<CellInfo>> m_grid;
|
||||
|
||||
int m_activeModuleIndex; // -1 = remove mode, -2 = no selection
|
||||
Rotation m_currentRotation;
|
||||
|
||||
std::vector<QPushButton*> m_moduleButtons;
|
||||
QPushButton* m_removeButton;
|
||||
QWidget* m_gridWidget;
|
||||
|
||||
std::optional<ShipLayoutConfig> m_result;
|
||||
};
|
||||
198
src/ui/ShipLayoutPreview.cpp
Normal file
198
src/ui/ShipLayoutPreview.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "ShipLayoutPreview.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const int kCellSize = 8;
|
||||
|
||||
const ModuleDef* findModuleDef(const std::vector<ModuleDef>& modules,
|
||||
const std::string& id)
|
||||
{
|
||||
for (const ModuleDef& def : modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
|
||||
{
|
||||
if (grid.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
const int srcH = static_cast<int>(grid.size());
|
||||
int srcW = 0;
|
||||
for (const std::string& row : grid)
|
||||
{
|
||||
const int w = static_cast<int>(row.size());
|
||||
if (w > srcW)
|
||||
{
|
||||
srcW = w;
|
||||
}
|
||||
}
|
||||
const int dstW = srcH;
|
||||
const int dstH = srcW;
|
||||
std::vector<std::string> dst(dstH, std::string(dstW, 'X'));
|
||||
for (int row = 0; row < srcH; ++row)
|
||||
{
|
||||
for (int col = 0; col < srcW; ++col)
|
||||
{
|
||||
const char ch = (col < static_cast<int>(grid[row].size()))
|
||||
? grid[row][col]
|
||||
: 'X';
|
||||
dst[col][srcH - 1 - row] = ch;
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
std::vector<std::string> rotateMask(const std::vector<std::string>& mask,
|
||||
Rotation rotation)
|
||||
{
|
||||
int steps = 0;
|
||||
switch (rotation)
|
||||
{
|
||||
case Rotation::East: steps = 0; break;
|
||||
case Rotation::South: steps = 1; break;
|
||||
case Rotation::West: steps = 2; break;
|
||||
case Rotation::North: steps = 3; break;
|
||||
}
|
||||
std::vector<std::string> result = mask;
|
||||
for (int i = 0; i < steps; ++i)
|
||||
{
|
||||
result = rotateMaskCW(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
ShipLayoutPreview::ShipLayoutPreview(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_modules(nullptr)
|
||||
, m_rows(0)
|
||||
, m_cols(0)
|
||||
{
|
||||
}
|
||||
|
||||
void ShipLayoutPreview::clear()
|
||||
{
|
||||
m_grid.clear();
|
||||
m_placedModules.clear();
|
||||
m_modules = nullptr;
|
||||
m_rows = 0;
|
||||
m_cols = 0;
|
||||
setFixedSize(0, 0);
|
||||
update();
|
||||
}
|
||||
|
||||
void ShipLayoutPreview::setShipAndLayout(const std::vector<std::string>& shipLayout,
|
||||
const ShipLayoutConfig& layout,
|
||||
const std::vector<ModuleDef>* modules)
|
||||
{
|
||||
m_modules = modules;
|
||||
m_placedModules = layout.placedModules;
|
||||
m_rows = static_cast<int>(shipLayout.size());
|
||||
m_cols = 0;
|
||||
for (const std::string& row : shipLayout)
|
||||
{
|
||||
const int w = static_cast<int>(row.size());
|
||||
if (w > m_cols)
|
||||
{
|
||||
m_cols = w;
|
||||
}
|
||||
}
|
||||
|
||||
m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, -1}));
|
||||
for (int r = 0; r < m_rows; ++r)
|
||||
{
|
||||
for (int c = 0; c < static_cast<int>(shipLayout[r].size()); ++c)
|
||||
{
|
||||
if (shipLayout[r][c] == 'O')
|
||||
{
|
||||
m_grid[r][c].buildable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_placedModules.size()); ++i)
|
||||
{
|
||||
const PlacedModule& pm = m_placedModules[i];
|
||||
const ModuleDef* def = findModuleDef(*m_modules, pm.moduleId);
|
||||
if (!def)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::vector<std::string> rotated = rotateMask(def->surfaceMask, pm.rotation);
|
||||
for (int mr = 0; mr < static_cast<int>(rotated.size()); ++mr)
|
||||
{
|
||||
for (int mc = 0; mc < static_cast<int>(rotated[mr].size()); ++mc)
|
||||
{
|
||||
if (rotated[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)
|
||||
{
|
||||
m_grid[gr][gc].moduleIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFixedSize(m_cols * kCellSize, m_rows * kCellSize);
|
||||
update();
|
||||
}
|
||||
|
||||
void ShipLayoutPreview::paintEvent(QPaintEvent* /*event*/)
|
||||
{
|
||||
if (m_rows == 0 || m_cols == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
for (int r = 0; r < m_rows; ++r)
|
||||
{
|
||||
for (int c = 0; c < m_cols; ++c)
|
||||
{
|
||||
const QRect cellRect(c * kCellSize, r * kCellSize, kCellSize, kCellSize);
|
||||
const CellInfo& cell = m_grid[r][c];
|
||||
|
||||
if (!cell.buildable)
|
||||
{
|
||||
painter.fillRect(cellRect, Qt::black);
|
||||
}
|
||||
else if (cell.moduleIndex >= 0)
|
||||
{
|
||||
const PlacedModule& pm = m_placedModules[cell.moduleIndex];
|
||||
const ModuleDef* def = findModuleDef(*m_modules, pm.moduleId);
|
||||
QColor color(Qt::gray);
|
||||
if (def)
|
||||
{
|
||||
color = QColor(QString::fromStdString(def->fillColor));
|
||||
}
|
||||
painter.fillRect(cellRect, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.fillRect(cellRect, Qt::white);
|
||||
}
|
||||
|
||||
painter.setPen(QColor(128, 128, 128));
|
||||
painter.drawRect(cellRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/ui/ShipLayoutPreview.h
Normal file
38
src/ui/ShipLayoutPreview.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "ModulesConfig.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
class ShipLayoutPreview : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ShipLayoutPreview(QWidget* parent = nullptr);
|
||||
|
||||
void setShipAndLayout(const std::vector<std::string>& shipLayout,
|
||||
const ShipLayoutConfig& layout,
|
||||
const std::vector<ModuleDef>* modules);
|
||||
void clear();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
struct CellInfo
|
||||
{
|
||||
bool buildable;
|
||||
int moduleIndex; // -1 if empty
|
||||
};
|
||||
|
||||
std::vector<std::vector<CellInfo>> m_grid;
|
||||
std::vector<PlacedModule> m_placedModules;
|
||||
const std::vector<ModuleDef>* m_modules;
|
||||
int m_rows;
|
||||
int m_cols;
|
||||
};
|
||||
Reference in New Issue
Block a user