move blueprints out of the sidebar into Ctrl+C / Ctrl+V dialogs

This commit is contained in:
2026-08-06 19:03:59 +02:00
parent 2cbcf1554f
commit 18cfe238f6
22 changed files with 1021 additions and 400 deletions

View File

@@ -56,7 +56,7 @@ static Rotation rotCCW(Rotation r)
return Rotation::East;
}
// Mirror of BlueprintPanel::createBlueprintFromSelection: given per-building
// Mirror of BlueprintLibrary::createBlueprintFromSelection: given per-building
// (anchor, bodyCells, type, rotation), compute Blueprint with floor-division
// bounding-box center and per-building tile offsets.
struct BuildingSpec
@@ -149,7 +149,7 @@ static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
}
}
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
// Mirrors BlueprintLibrary::createBlueprintFromSelection's player-placeable filter:
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
// are silently excluded before the bounding-box center and offsets are computed.
static Blueprint buildBlueprintFiltered(const std::vector<BuildingSpec>& specs,

View File

@@ -82,7 +82,7 @@ TEST_CASE("Only one mode is active at a time", "[buildmode]")
TEST_CASE("Entering builder mode announces that blueprint mode ended", "[buildmode]")
{
// Regression: entering builder mode used to drop the blueprint silently, so the
// blueprint panel kept its button highlighted for a mode that was over.
// blueprint library kept tracking an active blueprint for a mode that was over.
BuildModeController controller;
controller.enterBlueprintMode(makeBlueprint());

View File

@@ -139,3 +139,100 @@ TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]"
Simulation sim(loadTestConfig(), 7);
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
}
// The derived facts a blueprint card shows (REQ-UI-BLUEPRINT-CARD). They live here
// rather than in the dialog so they can be tested: src/ui is off the test include path.
namespace
{
Blueprint makeBlueprint(const std::vector<BuildingType>& types)
{
Blueprint blueprint;
for (const BuildingType type : types)
{
BlueprintBuilding building;
building.type = type;
building.rotation = Rotation::East;
blueprint.buildings.push_back(building);
}
return blueprint;
}
} // namespace
TEST_CASE("computeBlueprintCost sums the constituent placement costs", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
// belt 2 + belt 2 + miner 15.
const Blueprint blueprint = makeBlueprint(
{BuildingType::Belt, BuildingType::Belt, BuildingType::Miner});
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 19);
}
TEST_CASE("computeBlueprintCost ignores types absent from buildings.toml", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
REQUIRE(cfg.buildings.findBuildingDef(BuildingType::Hq) == nullptr);
const Blueprint blueprint = makeBlueprint({BuildingType::Belt, BuildingType::Hq});
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 2);
}
TEST_CASE("computeBlueprintCost is zero for an empty blueprint", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
CHECK(computeBlueprintCost(Blueprint{}, cfg.buildings) == 0);
}
TEST_CASE("summarizeBlueprintContents orders by descending count", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
const Blueprint blueprint = makeBlueprint(
{BuildingType::Belt, BuildingType::Smelter, BuildingType::Miner,
BuildingType::Miner, BuildingType::Smelter, BuildingType::Miner});
const std::vector<BlueprintContentEntry> summary =
summarizeBlueprintContents(blueprint, cfg.buildings);
REQUIRE(summary.size() == 3);
CHECK(summary[0].buildingName == "Miner");
CHECK(summary[0].count == 3);
CHECK(summary[1].buildingName == "Smelter");
CHECK(summary[1].count == 2);
CHECK(summary[2].buildingName == "Belt");
CHECK(summary[2].count == 1);
}
TEST_CASE("summarizeBlueprintContents breaks ties in buildings.toml order", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
// Miner is declared after Belt in buildings.toml, which is the order the build
// button bar lays its buttons out, so Belt wins the tie despite the equal counts
// and despite sorting later by enum value.
const Blueprint blueprint = makeBlueprint(
{BuildingType::Miner, BuildingType::Belt, BuildingType::Miner, BuildingType::Belt});
const std::vector<BlueprintContentEntry> summary =
summarizeBlueprintContents(blueprint, cfg.buildings);
REQUIRE(summary.size() == 2);
CHECK(summary[0].buildingName == "Belt");
CHECK(summary[1].buildingName == "Miner");
}
TEST_CASE("summarizeBlueprintContents spells multi-word ids as display names", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
const Blueprint blueprint = makeBlueprint({BuildingType::TunnelEntry});
const std::vector<BlueprintContentEntry> summary =
summarizeBlueprintContents(blueprint, cfg.buildings);
REQUIRE(summary.size() == 1);
CHECK(summary[0].buildingName == "Tunnel Entry");
}
TEST_CASE("summarizeBlueprintContents is empty for an empty blueprint", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
CHECK(summarizeBlueprintContents(Blueprint{}, cfg.buildings).empty());
}