#include "ShipLayoutDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include 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(std::toupper(static_cast(c))); nextUpper = false; } else { result += c; } } return result; } std::vector rotateMaskCW(const std::vector& grid) { if (grid.empty()) { return {}; } const int srcH = static_cast(grid.size()); int srcW = 0; for (const std::string& row : grid) { const int w = static_cast(row.size()); if (w > srcW) { srcW = w; } } const int dstW = srcH; const int dstH = srcW; std::vector 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(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>* grid, int rows, int cols, const std::vector* 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 mask = rotateMask(def.surfaceMask, m_ghostRotation); QColor ghostColor(QString::fromStdString(def.fillColor)); ghostColor.setAlpha(100); for (int mr = 0; mr < static_cast(mask.size()); ++mr) { for (int mc = 0; mc < static_cast(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 rotateMask(const std::vector& 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 result = mask; for (int i = 0; i < steps; ++i) { result = rotateMaskCW(result); } return result; } ShipLayoutDialog* m_dialog; const std::vector>* m_grid = nullptr; int m_rows = 0; int m_cols = 0; const std::vector* m_placed = nullptr; const GameConfig* m_config = nullptr; int m_ghostModuleIdx = -2; Rotation m_ghostRotation = Rotation::East; QPoint m_hoverCell = QPoint(-1, -1); }; // --------------------------------------------------------------------------- // Blueprint panel (third column of the layout dialog) // --------------------------------------------------------------------------- class ShipLayoutBlueprintPanel : public QWidget { public: ShipLayoutBlueprintPanel( std::vector& allBlueprints, const std::string& shipType, std::function()> getModules, std::function&)> 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(tr("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, tr("Create Blueprint"), tr("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 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(i)); rebuildList(); }); } } std::vector& m_allBlueprints; std::string m_shipType; std::function()> m_getModules; std::function&)> m_loadModules; QScrollArea* m_scrollArea = nullptr; QWidget* m_scrollContainer = nullptr; QVBoxLayout* m_listLayout = nullptr; }; // --------------------------------------------------------------------------- // ShipLayoutDialog implementation // --------------------------------------------------------------------------- ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, const std::string& shipId, const ShipLayoutConfig& currentLayout, std::vector& allBlueprints, 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(tr("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(m_shipLayout.size()); m_cols = 0; for (const std::string& row : m_shipLayout) { const int w = static_cast(row.size()); if (w > m_cols) { m_cols = w; } } // Initialize grid. m_grid.assign(m_rows, std::vector(m_cols, {false, -1})); for (int r = 0; r < m_rows; ++r) { for (int c = 0; c < static_cast(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(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(&QSignalMapper::mapped), this, &ShipLayoutDialog::onModuleButtonClicked); // Remove button. m_removeButton = new QPushButton(tr("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(tr("Confirm"), this); QPushButton* cancelBtn = new QPushButton(tr("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); // Right: blueprint panel (third column). ShipLayoutBlueprintPanel* bpPanel = new ShipLayoutBlueprintPanel( allBlueprints, m_shipId, [this]() { return m_placedModules; }, [this](const std::vector& mods) { loadLayoutBlueprint(mods); }, this); mainLayout->addWidget(bpPanel); // 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 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(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(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 mask = rotatedMask(*def, pm.rotation); for (int mr = 0; mr < static_cast(mask.size()); ++mr) { for (int mc = 0; mc < static_cast(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(m_gridWidget); gridW->setGhostData(m_activeModuleIndex, m_currentRotation); gridW->update(); } bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const { const std::vector mask = rotatedMask(def, rotation); for (int mr = 0; mr < static_cast(mask.size()); ++mr) { for (int mc = 0; mc < static_cast(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 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 result = def.surfaceMask; for (int i = 0; i < steps; ++i) { result = rotateMaskCW(result); } return result; } void ShipLayoutDialog::loadLayoutBlueprint(const std::vector& modules) { m_placedModules.clear(); // Build a temporary occupancy grid to detect overlaps within the blueprint. std::vector> occupied(m_rows, std::vector(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 mask = rotatedMask(*def, pm.rotation); bool valid = true; for (int mr = 0; mr < static_cast(mask.size()) && valid; ++mr) { for (int mc = 0; mc < static_cast(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(mask.size()); ++mr) { for (int mc = 0; mc < static_cast(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(); }