implement ship modules

This commit is contained in:
2026-05-18 08:49:51 +02:00
parent b59e392461
commit d08bf5d37b
33 changed files with 1911 additions and 56 deletions

616
src/ui/ShipLayoutDialog.cpp Normal file
View 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;
}