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

@@ -11,6 +11,10 @@ enemy_buffer_width = 10
schematic = "fighter" schematic = "fighter"
level = 1 level = 1
count = 5 count = 5
modules = [
{type = "weapon_upgrade", x = 0, y = 1, rotation = "east"},
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
]
[[arena.team]] [[arena.team]]
name = "Beta" name = "Beta"
@@ -18,6 +22,10 @@ enemy_buffer_width = 10
schematic = "sniper" schematic = "sniper"
level = 1 level = 1
count = 1 count = 1
modules = [
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
{type = "weapon_upgrade", x = 1, y = 2, rotation = "east"},
]
[[arena]] [[arena]]
@@ -33,6 +41,10 @@ enemy_buffer_width = 10
schematic = "sniper" schematic = "sniper"
level = 1 level = 1
count = 1 count = 1
modules = [
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
{type = "sensor_booster", x = 0, y = 1, rotation = "east"},
]
[[arena.team]] [[arena.team]]
name = "Beta" name = "Beta"
@@ -40,6 +52,11 @@ enemy_buffer_width = 10
schematic = "gunship" schematic = "gunship"
level = 1 level = 1
count = 1 count = 1
modules = [
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
{type = "weapon_upgrade", x = 3, y = 1, rotation = "east"},
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
]
[[arena]] [[arena]]
@@ -55,6 +72,11 @@ enemy_buffer_width = 10
schematic = "gunship" schematic = "gunship"
level = 1 level = 1
count = 1 count = 1
modules = [
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
{type = "weapon_upgrade", x = 3, y = 2, rotation = "east"},
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
]
[[arena.team]] [[arena.team]]
name = "Beta" name = "Beta"
@@ -62,6 +84,10 @@ enemy_buffer_width = 10
schematic = "fighter" schematic = "fighter"
level = 1 level = 1
count = 5 count = 5
modules = [
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
{type = "sensor_booster", x = 2, y = 1, rotation = "east"},
]
[[arena]] [[arena]]
@@ -77,6 +103,10 @@ enemy_buffer_width = 15
schematic = "fighter" schematic = "fighter"
level = 1 level = 1
count = 3 count = 3
modules = [
{type = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
]
[[arena.team.station]] [[arena.team.station]]
type = "player_station" type = "player_station"
x = 8 x = 8
@@ -94,3 +124,6 @@ enemy_buffer_width = 15
schematic = "fighter" schematic = "fighter"
level = 1 level = 1
count = 8 count = 8
modules = [
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
]

View File

@@ -11,6 +11,7 @@ Config files use the TOML format. The following config files drive game paramete
- **modules.toml** — per module type: id, surface mask, materials list, player production level, production time, threat cost, fill color, glyph, and stat modifier formulas (additive and/or multiplicative per stat). - **modules.toml** — per module type: id, surface mask, materials list, player production level, production time, threat cost, fill color, glyph, and stat modifier formulas (additive and/or multiplicative per stat).
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level. - **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it. - **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
- REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart. - REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart.
@@ -37,6 +38,26 @@ Ships in `ships.toml` define a `layout` — a list of strings that describes the
The layout grid determines which cells are available for module placement in the layout configuration dialog. The layout grid determines which cells are available for module placement in the layout configuration dialog.
### Layout Blueprint TOML Format
Each entry in `ship_layouts.toml` represents a named layout blueprint:
```toml
[[blueprint]]
name = "Heavy Shields" # display name; must be unique within a ship type
ship_type = "fighter" # matches a schematic id in ships.toml
modules = [
{type = "shield_module", x = 0, y = 0, rotation = 0},
{type = "cannon_module", x = 2, y = 1, rotation = 90},
]
```
- `name` — human-readable display name. Must be unique within a ship type; need not be globally unique.
- `ship_type` — the schematic id this blueprint belongs to. Must match a schematic defined in `ships.toml`.
- `modules` — array of placed module instances. Each entry: `type` (module id from `modules.toml`), `x` and `y` (zero-based column/row in the ship's layout grid), `rotation` (0, 90, 180, or 270 degrees clockwise). If `modules` is absent or empty, the blueprint represents an empty layout.
The `modules` array format is reused verbatim in `balancing.toml` ship entries (see REQ-BAL-TEAM).
### Module Surface Mask Format ### Module Surface Mask Format
Modules in `modules.toml` define a `surface_mask` — a list of strings that describes the module's tile footprint within the ship layout grid. Each character occupies one cell: Modules in `modules.toml` define a `surface_mask` — a list of strings that describes the module's tile footprint within the ship layout grid. Each character occupies one cell:
@@ -187,12 +208,25 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened. - REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened.
The dialog contains: The dialog contains:
- **Left side**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode. - **Left**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode.
- **Right side**: A grid of module selection buttons (one per module type defined in `modules.toml`) plus a "Remove" button. Each module button shows the module id and its glyph. - **Center**: A grid of module selection buttons (one per module type defined in `modules.toml`) plus a "Remove" button. Each module button shows the module id and its glyph.
- **Right**: The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed. - **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
- REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog). - REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog).
### Layout Blueprints
- REQ-MOD-UI-BLUEPRINT-PANEL: The right column of the layout configuration dialog is the layout blueprint panel. It shows only blueprints whose `ship_type` matches the schematic of the shipyard for which the dialog was opened. The panel contains, from top to bottom: a "Create Blueprint" button, followed by a scrollable list of blueprint entries (one per matching blueprint, in creation order).
- REQ-MOD-UI-BLUEPRINT-CREATE: Clicking "Create Blueprint" opens a modal dialog prompting for a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the module layout currently shown in the left-side layout grid (the in-progress state of the dialog, not the previously confirmed shipyard layout) and appends it to the blueprint list.
- REQ-MOD-UI-BLUEPRINT-ENTRY: Each blueprint entry shows the blueprint name and a delete icon ("×") to the right of the name. Clicking the entry (name area) loads that blueprint's module list into the left-side layout grid, replacing all currently placed modules. Module instances that are invalid for the current ship layout (unknown module type, position outside the grid, position on a non-buildable cell, or overlapping another module in the same blueprint) are silently skipped; the remaining valid instances are placed. Clicking the delete icon ("×") removes that blueprint entry from the list immediately.
- REQ-MOD-UI-BLUEPRINT-STARTUP: At application startup, layout blueprints are loaded from `ship_layouts.toml` in the same directory as the application executable. Blueprint entries missing required fields (`name` or `ship_type`) are silently skipped. If the file does not exist, the blueprint list starts empty with no error. If the file exists but cannot be parsed (malformed TOML), a modal error dialog describes the failure and the blueprint list starts empty.
- REQ-MOD-UI-BLUEPRINT-SHUTDOWN: On application shutdown, all layout blueprints (across all ship types) are written to `ship_layouts.toml` in the same directory as the application executable. The TOML structure matches the Layout Blueprint TOML Format. Write errors are silently ignored on shutdown.
## Defence Stations ## Defence Stations
- REQ-DEF-PLAYER-PLACEMENT: 2 player defence stations are pre-placed in space at the start. Their positions are determined by `world.toml [regions].asteroid_width` and `player_buffer_width`. - REQ-DEF-PLAYER-PLACEMENT: 2 player defence stations are pre-placed in space at the start. Their positions are determined by `world.toml [regions].asteroid_width` and `player_buffer_width`.
@@ -334,7 +368,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
- **World height** (in tiles). - **World height** (in tiles).
- Exactly **two teams**, each with a human-readable **team name**. - Exactly **two teams**, each with a human-readable **team name**.
- REQ-BAL-TEAM: Each team defines: - REQ-BAL-TEAM: Each team defines:
- A list of **ship entries**, each specifying: ship schematic (type), level, and count. - A list of **ship entries**, each specifying: ship schematic (type), level, count, and an optional `modules` array defining the module layout applied to every ship of that entry. The `modules` array format is identical to that used in `ship_layouts.toml` (see Layout Blueprint TOML Format). If `modules` is omitted, ships of that entry have no modules. Invalid module instances (unknown type, position outside the grid, position on a non-buildable cell, or overlapping another module in the same entry) are silently skipped during loading.
- An optional list of **defence station entries**, each specifying: station type (`player_station` or `enemy_station` from `stations.toml`), level, and tile position (x, y). - An optional list of **defence station entries**, each specifying: station type (`player_station` or `enemy_station` from `stations.toml`), level, and tile position (x, y).
- REQ-BAL-HQ: Each team has an HQ placed automatically at the vertical center of the arena at the far end of that team's buffer zone. HQ stats are read from `stations.toml [hq]` at level 1. Team 1's HQ is at the left edge; team 2's HQ is at the right edge. - REQ-BAL-HQ: Each team has an HQ placed automatically at the vertical center of the arena at the far end of that team's buffer zone. HQ stats are read from `stations.toml [hq]` at level 1. Team 1's HQ is at the left edge; team 2's HQ is at the right edge.
- REQ-BAL-SPAWN: Team 1's ships spawn in team 1's buffer zone (left side); team 2's ships spawn in team 2's buffer zone (right side). Spawn positions are uniformly random within the respective buffer zone. - REQ-BAL-SPAWN: Team 1's ships spawn in team 1's buffer zone (left side); team 2's ships spawn in team 2's buffer zone (right side). Spawn positions are uniformly random within the respective buffer zone.

View File

@@ -36,7 +36,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
m_beltSystem, m_beltSystem,
[this]() { return allocateId(); }, [this]() { return allocateId(); },
[](int) {}, [](int) {},
[](const std::string&, QVector2D) {}, [](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
m_rng); m_rng);
m_shipSystem = std::make_unique<ShipSystem>( m_shipSystem = std::make_unique<ShipSystem>(
@@ -198,7 +198,8 @@ void ArenaSimulation::spawnShips()
for (int i = 0; i < entry.count; ++i) for (int i = 0; i < entry.count; ++i)
{ {
const QVector2D pos(xDist(m_rng), yDist(m_rng)); const QVector2D pos(xDist(m_rng), yDist(m_rng));
m_shipSystem->spawn(entry.schematicId, entry.level, pos, false); m_shipSystem->spawn(entry.schematicId, entry.level, pos, false,
entry.layout);
} }
} }
} }
@@ -214,7 +215,8 @@ void ArenaSimulation::spawnShips()
for (int i = 0; i < entry.count; ++i) for (int i = 0; i < entry.count; ++i)
{ {
const QVector2D pos(xDist(m_rng), yDist(m_rng)); const QVector2D pos(xDist(m_rng), yDist(m_rng));
m_shipSystem->spawn(entry.schematicId, entry.level, pos, true); m_shipSystem->spawn(entry.schematicId, entry.level, pos, true,
entry.layout);
} }
} }
} }

View File

@@ -5,6 +5,8 @@
#include "toml.hpp" #include "toml.hpp"
#include "Rotation.h"
namespace namespace
{ {
@@ -35,6 +37,14 @@ std::string requireString(NodeView node, const std::string& path)
return *value; return *value;
} }
Rotation parseRotation(const std::string& s)
{
if (s == "east") { return Rotation::East; }
if (s == "south") { return Rotation::South; }
if (s == "west") { return Rotation::West; }
return Rotation::North;
}
} // namespace } // namespace
BalancingConfig loadBalancingConfig(const std::string& path) BalancingConfig loadBalancingConfig(const std::string& path)
@@ -119,6 +129,36 @@ BalancingConfig loadBalancingConfig(const std::string& path)
requireInt((*shipTbl)["level"], sPrefix + ".level")); requireInt((*shipTbl)["level"], sPrefix + ".level"));
entry.count = static_cast<int>( entry.count = static_cast<int>(
requireInt((*shipTbl)["count"], sPrefix + ".count")); requireInt((*shipTbl)["count"], sPrefix + ".count"));
const toml::array* modArray = (*shipTbl)["modules"].as_array();
if (modArray && !modArray->empty())
{
ShipLayoutConfig layout;
for (std::size_t mi = 0; mi < modArray->size(); ++mi)
{
const toml::table* modTbl = (*modArray)[mi].as_table();
if (!modTbl) { continue; }
const std::optional<std::string> type =
(*modTbl)["type"].value<std::string>();
const std::optional<int64_t> x =
(*modTbl)["x"].value<int64_t>();
const std::optional<int64_t> y =
(*modTbl)["y"].value<int64_t>();
const std::optional<std::string> rotStr =
(*modTbl)["rotation"].value<std::string>();
if (!type || !x || !y || !rotStr) { continue; }
PlacedModule pm;
pm.moduleId = *type;
pm.position = QPoint(static_cast<int>(*x),
static_cast<int>(*y));
pm.rotation = parseRotation(*rotStr);
layout.placedModules.push_back(std::move(pm));
}
entry.layout = std::move(layout);
}
team.ships.push_back(entry); team.ships.push_back(entry);
} }
} }

View File

@@ -1,10 +1,13 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include <QPoint> #include <QPoint>
#include "ShipLayout.h"
struct ArenaStationEntry struct ArenaStationEntry
{ {
std::string stationType; // "player_station" or "enemy_station" std::string stationType; // "player_station" or "enemy_station"
@@ -17,6 +20,7 @@ struct ArenaShipEntry
std::string schematicId; std::string schematicId;
int level; int level;
int count; int count;
std::optional<ShipLayoutConfig> layout;
}; };
struct ArenaTeamConfig struct ArenaTeamConfig

View File

@@ -11,6 +11,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h ${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.h
PARENT_SCOPE PARENT_SCOPE
) )
@@ -20,6 +21,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.cpp
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -0,0 +1,129 @@
#include "ShipLayoutBlueprintSerializer.h"
#include <sstream>
#include <stdexcept>
#include "toml.hpp"
#include "Rotation.h"
namespace
{
std::string rotationToString(Rotation r)
{
switch (r)
{
case Rotation::North: return "north";
case Rotation::East: return "east";
case Rotation::South: return "south";
case Rotation::West: return "west";
}
return "north";
}
Rotation parseRotation(const std::string& s)
{
if (s == "east") { return Rotation::East; }
if (s == "south") { return Rotation::South; }
if (s == "west") { return Rotation::West; }
return Rotation::North;
}
} // namespace
namespace ShipLayoutBlueprintSerializer
{
std::string serialize(const std::vector<ShipLayoutBlueprint>& blueprints)
{
toml::array bpArr;
for (const ShipLayoutBlueprint& bp : blueprints)
{
toml::array modArr;
for (const PlacedModule& pm : bp.modules)
{
toml::table modTbl;
modTbl.insert("type", pm.moduleId);
modTbl.insert("x", static_cast<int64_t>(pm.position.x()));
modTbl.insert("y", static_cast<int64_t>(pm.position.y()));
modTbl.insert("rotation", rotationToString(pm.rotation));
modArr.push_back(std::move(modTbl));
}
toml::table bpTbl;
bpTbl.insert("name", bp.name.toStdString());
bpTbl.insert("ship_type", bp.shipType);
bpTbl.insert("modules", std::move(modArr));
bpArr.push_back(std::move(bpTbl));
}
toml::table root;
root.insert("blueprint", std::move(bpArr));
std::ostringstream oss;
oss << root;
return oss.str();
}
std::vector<ShipLayoutBlueprint> deserialize(const std::string& tomlContent)
{
toml::table root;
try
{
root = toml::parse(tomlContent);
}
catch (const toml::parse_error& e)
{
std::ostringstream msg;
msg << "TOML parse error: " << e.description() << " at " << e.source().begin;
throw std::runtime_error(msg.str());
}
std::vector<ShipLayoutBlueprint> result;
const toml::array* bpArr = root["blueprint"].as_array();
if (!bpArr) { return result; }
for (std::size_t i = 0; i < bpArr->size(); ++i)
{
const toml::table* bpTbl = (*bpArr)[i].as_table();
if (!bpTbl) { continue; }
const std::optional<std::string> name = (*bpTbl)["name"].value<std::string>();
const std::optional<std::string> shipType = (*bpTbl)["ship_type"].value<std::string>();
if (!name || name->empty() || !shipType || shipType->empty()) { continue; }
ShipLayoutBlueprint bp;
bp.name = QString::fromStdString(*name);
bp.shipType = *shipType;
const toml::array* modArr = (*bpTbl)["modules"].as_array();
if (modArr)
{
for (std::size_t j = 0; j < modArr->size(); ++j)
{
const toml::table* modTbl = (*modArr)[j].as_table();
if (!modTbl) { continue; }
const std::optional<std::string> type = (*modTbl)["type"].value<std::string>();
const std::optional<int64_t> x = (*modTbl)["x"].value<int64_t>();
const std::optional<int64_t> y = (*modTbl)["y"].value<int64_t>();
const std::optional<std::string> rotStr = (*modTbl)["rotation"].value<std::string>();
if (!type || !x || !y || !rotStr) { continue; }
PlacedModule pm;
pm.moduleId = *type;
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
pm.rotation = parseRotation(*rotStr);
bp.modules.push_back(std::move(pm));
}
}
result.push_back(std::move(bp));
}
return result;
}
} // namespace ShipLayoutBlueprintSerializer

View File

@@ -0,0 +1,14 @@
#pragma once
#include <string>
#include <vector>
#include "ShipLayoutBlueprint.h"
namespace ShipLayoutBlueprintSerializer
{
std::string serialize(const std::vector<ShipLayoutBlueprint>& blueprints);
std::vector<ShipLayoutBlueprint> deserialize(const std::string& tomlContent);
} // namespace ShipLayoutBlueprintSerializer

View File

@@ -8,6 +8,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/Scrap.h ${CMAKE_CURRENT_SOURCE_DIR}/Scrap.h
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h ${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h

View File

@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
#include <QString>
#include "ShipLayout.h"
struct ShipLayoutBlueprint
{
QString name;
std::string shipType;
std::vector<PlacedModule> modules;
};

View File

@@ -1,6 +1,8 @@
#include "MainWindow.h" #include "MainWindow.h"
#include <QApplication> #include <QApplication>
#include <QCloseEvent>
#include <QFile>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
@@ -14,6 +16,7 @@
#include "GameWorldView.h" #include "GameWorldView.h"
#include "HeaderBar.h" #include "HeaderBar.h"
#include "SelectedBuildingPanel.h" #include "SelectedBuildingPanel.h"
#include "ShipLayoutBlueprintSerializer.h"
#include "ShipLayoutDialog.h" #include "ShipLayoutDialog.h"
#include "Simulation.h" #include "Simulation.h"
#include "Tick.h" #include "Tick.h"
@@ -111,6 +114,24 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
m_gameWorldView->setFocus(); 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) void MainWindow::resizeEvent(QResizeEvent* event)
@@ -119,6 +140,23 @@ void MainWindow::resizeEvent(QResizeEvent* event)
layoutPanels(); 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() void MainWindow::layoutPanels()
{ {
const int totalW = width(); const int totalW = width();
@@ -199,7 +237,8 @@ void MainWindow::onLayoutDialogRequested(EntityId shipyardId)
currentLayout = *b->shipLayout; 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()) if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
{ {
m_sim->buildings().setShipLayout(shipyardId, *dialog.result()); m_sim->buildings().setShipLayout(shipyardId, *dialog.result());

View File

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

View File

@@ -1,13 +1,16 @@
#include "ShipLayoutDialog.h" #include "ShipLayoutDialog.h"
#include <cctype> #include <cctype>
#include <functional>
#include <QGridLayout> #include <QGridLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QInputDialog>
#include <QKeyEvent> #include <QKeyEvent>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPushButton> #include <QPushButton>
#include <QScrollArea>
#include <QSignalMapper> #include <QSignalMapper>
#include <QVBoxLayout> #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 // ShipLayoutDialog implementation
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -270,6 +385,7 @@ private:
ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
const std::string& shipId, const std::string& shipId,
const ShipLayoutConfig& currentLayout, const ShipLayoutConfig& currentLayout,
std::vector<ShipLayoutBlueprint>& allBlueprints,
QWidget* parent) QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_config(config) , m_config(config)
@@ -408,6 +524,15 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
mainLayout->addLayout(rightLayout); 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. // Grid click handler.
connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) { connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) {
if (m_activeModuleIndex == -2) if (m_activeModuleIndex == -2)
@@ -614,3 +739,60 @@ std::vector<std::string> ShipLayoutDialog::rotatedMask(const ModuleDef& def,
} }
return result; 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 "GameConfig.h"
#include "Rotation.h" #include "Rotation.h"
#include "ShipLayout.h" #include "ShipLayout.h"
#include "ShipLayoutBlueprint.h"
class QPushButton; class QPushButton;
@@ -21,6 +22,7 @@ public:
ShipLayoutDialog(const GameConfig* config, ShipLayoutDialog(const GameConfig* config,
const std::string& shipId, const std::string& shipId,
const ShipLayoutConfig& currentLayout, const ShipLayoutConfig& currentLayout,
std::vector<ShipLayoutBlueprint>& allBlueprints,
QWidget* parent = nullptr); QWidget* parent = nullptr);
std::optional<ShipLayoutConfig> result() const; std::optional<ShipLayoutConfig> result() const;
@@ -49,6 +51,7 @@ private:
void updateGridWidget(); void updateGridWidget();
bool canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const; bool canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const;
std::vector<std::string> rotatedMask(const ModuleDef& def, 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; const GameConfig* m_config;
std::string m_shipId; std::string m_shipId;