From 541b8fdaeea174c83903657e492f4b987e5c0d6f Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Mon, 27 Apr 2026 12:58:44 +0200 Subject: [PATCH] implement storing recipes in blueprint --- src/lib/core/Blueprint.h | 4 +- src/test/BlueprintTest.cpp | 84 ++++++++++++++++++++++++++++++++++++++ src/ui/BlueprintPanel.cpp | 1 + src/ui/GameWorldView.cpp | 15 ++++++- 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/lib/core/Blueprint.h b/src/lib/core/Blueprint.h index b1f0ee8..c2b679c 100644 --- a/src/lib/core/Blueprint.h +++ b/src/lib/core/Blueprint.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,7 +13,8 @@ struct BlueprintBuilding { BuildingType type; 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 }; struct Blueprint diff --git a/src/test/BlueprintTest.cpp b/src/test/BlueprintTest.cpp index 61927ba..90d4647 100644 --- a/src/test/BlueprintTest.cpp +++ b/src/test/BlueprintTest.cpp @@ -7,6 +7,7 @@ #include #include "Blueprint.h" +#include "Building.h" #include "BuildingsConfig.h" #include "BuildingSystem.h" #include "BuildingType.h" @@ -15,6 +16,7 @@ #include "Rotation.h" #include "Simulation.h" #include "SurfaceMask.h" +#include "Tick.h" // --------------------------------------------------------------------------- // Helpers that mirror the production implementations under test. @@ -58,6 +60,7 @@ struct BuildingSpec std::vector bodyCells; BuildingType type; Rotation rotation; + std::string recipeId; // empty = none }; static Blueprint buildBlueprint(const std::vector& specs) @@ -83,6 +86,7 @@ static Blueprint buildBlueprint(const std::vector& specs) bb.type = s.type; bb.rotation = s.rotation; bb.offset = s.anchor - center; + bb.recipeId = s.recipeId; bp.buildings.push_back(bb); } return bp; @@ -581,3 +585,83 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidEntityId and REQUIRE(id == kInvalidEntityId); REQUIRE(sim.buildingBlocksStock() == blocksBeforeAttempt); } + +// --------------------------------------------------------------------------- +// Recipe / schematic capture and re-application +// --------------------------------------------------------------------------- + +TEST_CASE("Blueprint: recipeId is stored in BlueprintBuilding", "[blueprint]") +{ + const BuildingSpec spec{ + QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Miner, Rotation::East, + "mine_iron_ore" + }; + const Blueprint bp = buildBlueprint({ spec }); + + REQUIRE(bp.buildings.size() == 1); + REQUIRE(bp.buildings[0].recipeId == "mine_iron_ore"); +} + +TEST_CASE("Blueprint: building with no recipe has empty recipeId", "[blueprint]") +{ + const BuildingSpec spec{ + QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East + // recipeId defaults to "" + }; + const Blueprint bp = buildBlueprint({ spec }); + + REQUIRE(bp.buildings[0].recipeId.empty()); +} + +TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]") +{ + Simulation sim(loadConfig()); + + // Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid. + const EntityId id = sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(id != kInvalidEntityId); + + sim.buildings().setRecipe(id, "mine_iron_ore"); + + const ConstructionSite* site = sim.buildings().findSite(id); + REQUIRE(site != nullptr); + REQUIRE(site->recipeId == "mine_iron_ore"); +} + +TEST_CASE("Blueprint placement: recipe transfers to building after construction completes", + "[blueprint]") +{ + Simulation sim(loadConfig()); + + const EntityId id = sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(id != kInvalidEntityId); + sim.buildings().setRecipe(id, "mine_copper_ore"); + + // Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300. + // Run 301 ticks (0..300) to process the completion tick. + for (int i = 0; i <= static_cast(secondsToTicks(10.0)); ++i) + { + sim.tick(); + } + + const Building* b = sim.buildings().findBuilding(id); + REQUIRE(b != nullptr); + REQUIRE(b->recipeId == "mine_copper_ore"); +} + +TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]") +{ + // "interceptor" has available_from_start = true in the test config. + // This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics. + Simulation sim(loadConfig()); + REQUIRE(sim.isSchematicUnlocked("interceptor")); +} + +TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start", "[blueprint]") +{ + // "repair_ship" has available_from_start = false in the test config. + // This confirms the guard in placeBlueprintAtTile blocks locked schematics, + // leaving the shipyard's schematic unset. + Simulation sim(loadConfig()); + REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship")); +} diff --git a/src/ui/BlueprintPanel.cpp b/src/ui/BlueprintPanel.cpp index dd5aa13..c6f760b 100644 --- a/src/ui/BlueprintPanel.cpp +++ b/src/ui/BlueprintPanel.cpp @@ -188,6 +188,7 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const bb.type = e.building->type; bb.rotation = e.building->rotation; bb.offset = e.building->anchor - center; + bb.recipeId = e.building->recipeId; bp.buildings.push_back(bb); } return bp; diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 8a028e7..ca13d26 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -472,7 +472,20 @@ void GameWorldView::placeBlueprintAtTile(QPoint center) for (const BlueprintBuilding& bb : bp.buildings) { - m_sim->tryPlaceBuilding(bb.type, center + bb.offset, bb.rotation); + const EntityId id = m_sim->tryPlaceBuilding(bb.type, center + bb.offset, bb.rotation); + if (id == kInvalidEntityId || bb.recipeId.empty()) { continue; } + + if (bb.type == BuildingType::Shipyard) + { + if (m_sim->isSchematicUnlocked(bb.recipeId)) + { + m_sim->buildings().setRecipe(id, bb.recipeId); + } + } + else + { + m_sim->buildings().setRecipe(id, bb.recipeId); + } } }