store ship module layout in shipyard blueprint
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -50,6 +51,20 @@ std::string serialize(const std::vector<Blueprint>& blueprints)
|
|||||||
bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x()));
|
bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x()));
|
||||||
bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y()));
|
bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y()));
|
||||||
bldTbl.insert("recipe_id", b.recipeId);
|
bldTbl.insert("recipe_id", b.recipeId);
|
||||||
|
if (b.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
toml::array modArr;
|
||||||
|
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
bldTbl.insert("modules", std::move(modArr));
|
||||||
|
}
|
||||||
bldArr.push_back(std::move(bldTbl));
|
bldArr.push_back(std::move(bldTbl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +138,27 @@ std::vector<Blueprint> deserialize(const std::string& tomlContent)
|
|||||||
bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
|
bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
|
||||||
bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0})));
|
bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0})));
|
||||||
bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{});
|
bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{});
|
||||||
|
const toml::array* modArr = (*bldTbl)["modules"].as_array();
|
||||||
|
if (modArr)
|
||||||
|
{
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
for (std::size_t k = 0; k < modArr->size(); ++k)
|
||||||
|
{
|
||||||
|
const toml::table* modTbl = (*modArr)[k].as_table();
|
||||||
|
if (!modTbl) { continue; }
|
||||||
|
const std::optional<std::string> modType = (*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 (!modType || !x || !y || !rotStr) { continue; }
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = *modType;
|
||||||
|
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||||
|
pm.rotation = parseRotation(*rotStr);
|
||||||
|
layout.placedModules.push_back(std::move(pm));
|
||||||
|
}
|
||||||
|
bb.shipLayout = std::move(layout);
|
||||||
|
}
|
||||||
bp.buildings.push_back(std::move(bb));
|
bp.buildings.push_back(std::move(bb));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
|
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
struct BlueprintBuilding
|
struct BlueprintBuilding
|
||||||
{
|
{
|
||||||
@@ -15,6 +17,7 @@ struct BlueprintBuilding
|
|||||||
Rotation rotation;
|
Rotation rotation;
|
||||||
QPoint offset; // tile offset from bounding-box center (floor for even sizes)
|
QPoint offset; // tile offset from bounding-box center (floor for even sizes)
|
||||||
std::string recipeId; // empty = none selected
|
std::string recipeId; // empty = none selected
|
||||||
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Blueprint
|
struct Blueprint
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
#include "BlueprintSerializer.h"
|
#include "BlueprintSerializer.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -26,13 +28,15 @@ Blueprint makeBlueprintWith(const std::vector<BlueprintBuilding>& buildings,
|
|||||||
BlueprintBuilding makeBuilding(BuildingType type,
|
BlueprintBuilding makeBuilding(BuildingType type,
|
||||||
Rotation rotation,
|
Rotation rotation,
|
||||||
QPoint offset,
|
QPoint offset,
|
||||||
std::string recipeId = "")
|
std::string recipeId = "",
|
||||||
|
std::optional<ShipLayoutConfig> shipLayout = std::nullopt)
|
||||||
{
|
{
|
||||||
BlueprintBuilding b;
|
BlueprintBuilding b;
|
||||||
b.type = type;
|
b.type = type;
|
||||||
b.rotation = rotation;
|
b.rotation = rotation;
|
||||||
b.offset = offset;
|
b.offset = offset;
|
||||||
b.recipeId = std::move(recipeId);
|
b.recipeId = std::move(recipeId);
|
||||||
|
b.shipLayout = std::move(shipLayout);
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,3 +189,77 @@ TEST_CASE("BlueprintSerializer: unknown building type throws", "[serializer]")
|
|||||||
BlueprintSerializer::deserialize(badToml),
|
BlueprintSerializer::deserialize(badToml),
|
||||||
std::runtime_error);
|
std::runtime_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ship layout round-trip through TOML serialization
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: shipyard with modules round-trips ship layout", "[serializer]")
|
||||||
|
{
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pmA;
|
||||||
|
pmA.moduleId = "laser_cannon";
|
||||||
|
pmA.position = QPoint(1, 1);
|
||||||
|
pmA.rotation = Rotation::East;
|
||||||
|
PlacedModule pmB;
|
||||||
|
pmB.moduleId = "sensor_booster";
|
||||||
|
pmB.position = QPoint(0, 1);
|
||||||
|
pmB.rotation = Rotation::South;
|
||||||
|
layout.placedModules = { pmA, pmB };
|
||||||
|
|
||||||
|
const Blueprint original = makeBlueprintWith(
|
||||||
|
{ makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0),
|
||||||
|
"interceptor", layout) },
|
||||||
|
"Gunship");
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
|
||||||
|
const BlueprintBuilding& b = loaded[0].buildings[0];
|
||||||
|
REQUIRE(b.shipLayout.has_value());
|
||||||
|
REQUIRE(b.shipLayout->placedModules.size() == 2);
|
||||||
|
REQUIRE(b.shipLayout->placedModules[0].moduleId == "laser_cannon");
|
||||||
|
REQUIRE(b.shipLayout->placedModules[0].position == QPoint(1, 1));
|
||||||
|
REQUIRE(b.shipLayout->placedModules[0].rotation == Rotation::East);
|
||||||
|
REQUIRE(b.shipLayout->placedModules[1].moduleId == "sensor_booster");
|
||||||
|
REQUIRE(b.shipLayout->placedModules[1].position == QPoint(0, 1));
|
||||||
|
REQUIRE(b.shipLayout->placedModules[1].rotation == Rotation::South);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: building without ship layout round-trips with nullopt", "[serializer]")
|
||||||
|
{
|
||||||
|
// A shipyard with no shipLayout set — no modules key should be emitted,
|
||||||
|
// and deserialization must leave shipLayout as nullopt.
|
||||||
|
const Blueprint original = makeBlueprintWith(
|
||||||
|
{ makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0), "interceptor") },
|
||||||
|
"EmptyYard");
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
REQUIRE_FALSE(loaded[0].buildings[0].shipLayout.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: shipyard with empty modules list round-trips", "[serializer]")
|
||||||
|
{
|
||||||
|
// An explicitly empty layout (player cleared all modules) should round-trip
|
||||||
|
// as a present but empty ShipLayoutConfig, distinct from nullopt.
|
||||||
|
ShipLayoutConfig emptyLayout;
|
||||||
|
const Blueprint original = makeBlueprintWith(
|
||||||
|
{ makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0),
|
||||||
|
"interceptor", emptyLayout) },
|
||||||
|
"EmptyLayout");
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
|
||||||
|
REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -61,6 +62,7 @@ struct BuildingSpec
|
|||||||
BuildingType type;
|
BuildingType type;
|
||||||
Rotation rotation;
|
Rotation rotation;
|
||||||
std::string recipeId; // empty = none
|
std::string recipeId; // empty = none
|
||||||
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
static Blueprint buildBlueprint(const std::vector<BuildingSpec>& specs)
|
static Blueprint buildBlueprint(const std::vector<BuildingSpec>& specs)
|
||||||
@@ -87,6 +89,7 @@ static Blueprint buildBlueprint(const std::vector<BuildingSpec>& specs)
|
|||||||
bb.rotation = s.rotation;
|
bb.rotation = s.rotation;
|
||||||
bb.offset = s.anchor - center;
|
bb.offset = s.anchor - center;
|
||||||
bb.recipeId = s.recipeId;
|
bb.recipeId = s.recipeId;
|
||||||
|
bb.shipLayout = s.shipLayout;
|
||||||
bp.buildings.push_back(bb);
|
bp.buildings.push_back(bb);
|
||||||
}
|
}
|
||||||
return bp;
|
return bp;
|
||||||
@@ -665,3 +668,108 @@ TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start",
|
|||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ship layout capture and re-application via blueprints
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint: shipLayout is captured in BlueprintBuilding", "[blueprint]")
|
||||||
|
{
|
||||||
|
// sensor_booster has surface_mask ["O"] — fits a single buildable cell.
|
||||||
|
// The interceptor layout ["XOX","OOO","XOX"] has a buildable cell at (1, 0).
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "sensor_booster";
|
||||||
|
pm.position = QPoint(1, 0);
|
||||||
|
pm.rotation = Rotation::North;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
const BuildingSpec spec{
|
||||||
|
QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Shipyard, Rotation::East,
|
||||||
|
"interceptor", layout
|
||||||
|
};
|
||||||
|
const Blueprint bp = buildBlueprint({ spec });
|
||||||
|
|
||||||
|
REQUIRE(bp.buildings.size() == 1);
|
||||||
|
REQUIRE(bp.buildings[0].shipLayout.has_value());
|
||||||
|
REQUIRE(bp.buildings[0].shipLayout->placedModules.size() == 1);
|
||||||
|
REQUIRE(bp.buildings[0].shipLayout->placedModules[0].moduleId == "sensor_booster");
|
||||||
|
REQUIRE(bp.buildings[0].shipLayout->placedModules[0].position == QPoint(1, 0));
|
||||||
|
REQUIRE(bp.buildings[0].shipLayout->placedModules[0].rotation == Rotation::North);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint: building without layout has nullopt shipLayout", "[blueprint]")
|
||||||
|
{
|
||||||
|
const BuildingSpec spec{
|
||||||
|
QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Shipyard, Rotation::East, "interceptor"
|
||||||
|
};
|
||||||
|
const Blueprint bp = buildBlueprint({ spec });
|
||||||
|
|
||||||
|
REQUIRE(bp.buildings.size() == 1);
|
||||||
|
REQUIRE_FALSE(bp.buildings[0].shipLayout.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
// Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
|
||||||
|
// A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
|
||||||
|
// S-tile at (0,0) and (0,1) — x >= 0, valid space tiles.
|
||||||
|
const BuildingId id = sim.tryPlaceBuilding(BuildingType::Shipyard, QPoint(-3, 0), Rotation::East);
|
||||||
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "laser_cannon";
|
||||||
|
pm.position = QPoint(1, 1);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
sim.buildings().setShipLayout(id, layout);
|
||||||
|
|
||||||
|
const ConstructionSite* site = sim.buildings().findSite(id);
|
||||||
|
REQUIRE(site != nullptr);
|
||||||
|
REQUIRE(site->shipLayout.has_value());
|
||||||
|
REQUIRE(site->shipLayout->placedModules.size() == 1);
|
||||||
|
REQUIRE(site->shipLayout->placedModules[0].moduleId == "laser_cannon");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes",
|
||||||
|
"[blueprint]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
const BuildingId id = sim.tryPlaceBuilding(BuildingType::Shipyard, QPoint(-3, 0), Rotation::East);
|
||||||
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "sensor_booster";
|
||||||
|
pm.position = QPoint(1, 0);
|
||||||
|
pm.rotation = Rotation::North;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
sim.buildings().setShipLayout(id, layout);
|
||||||
|
|
||||||
|
// Shipyard construction_time_seconds = 30 in the test config.
|
||||||
|
double constructionTime = 0.0;
|
||||||
|
for (const BuildingDef& def : sim.config().buildings.buildings)
|
||||||
|
{
|
||||||
|
if (def.type == BuildingType::Shipyard) { constructionTime = def.constructionTimeSeconds; break; }
|
||||||
|
}
|
||||||
|
REQUIRE(constructionTime > 0.0);
|
||||||
|
|
||||||
|
for (int i = 0; i <= static_cast<int>(secondsToTicks(constructionTime)); ++i)
|
||||||
|
{
|
||||||
|
sim.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Building* b = sim.buildings().findBuilding(id);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
REQUIRE(b->shipLayout.has_value());
|
||||||
|
REQUIRE(b->shipLayout->placedModules.size() == 1);
|
||||||
|
REQUIRE(b->shipLayout->placedModules[0].moduleId == "sensor_booster");
|
||||||
|
REQUIRE(b->shipLayout->placedModules[0].position == QPoint(1, 0));
|
||||||
|
REQUIRE(b->shipLayout->placedModules[0].rotation == Rotation::North);
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
|||||||
bb.rotation = e.building->rotation;
|
bb.rotation = e.building->rotation;
|
||||||
bb.offset = e.building->anchor - center;
|
bb.offset = e.building->anchor - center;
|
||||||
bb.recipeId = e.building->recipeId;
|
bb.recipeId = e.building->recipeId;
|
||||||
|
bb.shipLayout = e.building->shipLayout;
|
||||||
bp.buildings.push_back(bb);
|
bp.buildings.push_back(bb);
|
||||||
}
|
}
|
||||||
return bp;
|
return bp;
|
||||||
|
|||||||
@@ -504,8 +504,10 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BuildingId id = m_sim->tryPlaceBuilding(bb.type, anchor, bb.rotation);
|
const BuildingId id = m_sim->tryPlaceBuilding(bb.type, anchor, bb.rotation);
|
||||||
if (id == kInvalidBuildingId || bb.recipeId.empty()) { continue; }
|
if (id == kInvalidBuildingId) { continue; }
|
||||||
|
|
||||||
|
if (!bb.recipeId.empty())
|
||||||
|
{
|
||||||
if (bb.type == BuildingType::Shipyard)
|
if (bb.type == BuildingType::Shipyard)
|
||||||
{
|
{
|
||||||
if (m_sim->isSchematicUnlocked(bb.recipeId))
|
if (m_sim->isSchematicUnlocked(bb.recipeId))
|
||||||
@@ -518,6 +520,12 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
m_sim->buildings().setRecipe(id, bb.recipeId);
|
m_sim->buildings().setRecipe(id, bb.recipeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bb.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
m_sim->buildings().setShipLayout(id, *bb.shipLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::placeAtTile(QPoint tile)
|
void GameWorldView::placeAtTile(QPoint tile)
|
||||||
|
|||||||
Reference in New Issue
Block a user