diff --git a/src/lib/sim/BuildingConfig.cpp b/src/lib/sim/BuildingConfig.cpp index bc8b994..8d1bb90 100644 --- a/src/lib/sim/BuildingConfig.cpp +++ b/src/lib/sim/BuildingConfig.cpp @@ -1,10 +1,55 @@ #include "BuildingConfig.h" +#include +#include + #include "BeltSystem.h" #include "Building.h" #include "BuildingSystem.h" #include "Simulation.h" +namespace +{ +// The blueprint-relevant geometry shared by operational buildings and construction +// sites. Resolved from whichever of the two a selected id refers to. +struct SelectedBuilding +{ + BuildingId id; + BuildingType type; + Rotation rotation; + QPoint anchor; + const std::vector* bodyCells; +}; + +// Resolves a selected id to a player-placeable building or construction site, if it +// is one. Returns std::nullopt for an unknown id or a non-player-placeable building +// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE). +std::optional resolvePlaceable(const Simulation& sim, BuildingId id) +{ + const Building* building = sim.buildings().findBuilding(id); + const ConstructionSite* site = building ? nullptr : sim.buildings().findSite(id); + if (!building && !site) + { + return std::nullopt; + } + + const BuildingType type = building ? building->type : site->type; + const BuildingDef* def = sim.config().buildings.findBuildingDef(type); + if (!def || !def->playerPlaceable) + { + return std::nullopt; + } + + SelectedBuilding resolved; + resolved.id = id; + resolved.type = type; + resolved.rotation = building ? building->rotation : site->rotation; + resolved.anchor = building ? building->anchor : site->anchor; + resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells; + return resolved; +} +} // namespace + std::optional readBuildingConfig(const Simulation& sim, BuildingId id) { const Building* building = sim.buildings().findBuilding(id); @@ -49,3 +94,74 @@ std::optional readBuildingConfig(const Simulation& sim, Building return config; } + +Blueprint captureBlueprintFromSelection(const Simulation& sim, + const std::vector& selectedIds) +{ + std::vector entries; + entries.reserve(selectedIds.size()); + for (const BuildingId id : selectedIds) + { + const std::optional resolved = resolvePlaceable(sim, id); + if (resolved.has_value()) + { + entries.push_back(*resolved); + } + } + + if (entries.empty()) + { + return Blueprint{}; + } + + int minX = INT_MAX, maxX = INT_MIN; + int minY = INT_MAX, maxY = INT_MIN; + for (const SelectedBuilding& e : entries) + { + for (const QPoint& cell : *e.bodyCells) + { + minX = std::min(minX, cell.x()); + maxX = std::max(maxX, cell.x()); + minY = std::min(minY, cell.y()); + maxY = std::max(maxY, cell.y()); + } + } + + const QPoint center((minX + maxX) / 2, (minY + maxY) / 2); + + Blueprint blueprint; + blueprint.buildings.reserve(entries.size()); + for (const SelectedBuilding& e : entries) + { + BlueprintBuilding building; + building.type = e.type; + building.rotation = e.rotation; + building.offset = e.anchor - center; + // Recipe / schematic / layout / splitter-filter capture is shared with the + // copy-settings gesture (REQ-BLD-COPY-CONFIG) via readBuildingConfig, which + // handles operational buildings and construction sites alike. + const std::optional config = readBuildingConfig(sim, e.id); + if (config.has_value()) + { + building.recipeId = config->recipeId.value_or(std::string()); + building.shipLayout = config->shipLayout; + building.splitterFilterA = config->splitterFilterA; + building.splitterFilterB = config->splitterFilterB; + } + blueprint.buildings.push_back(building); + } + return blueprint; +} + +bool selectionHasPlaceableBuilding(const Simulation& sim, + const std::vector& selectedIds) +{ + for (const BuildingId id : selectedIds) + { + if (resolvePlaceable(sim, id).has_value()) + { + return true; + } + } + return false; +} diff --git a/src/lib/sim/BuildingConfig.h b/src/lib/sim/BuildingConfig.h index c9c3840..d333372 100644 --- a/src/lib/sim/BuildingConfig.h +++ b/src/lib/sim/BuildingConfig.h @@ -4,6 +4,7 @@ #include #include +#include "Blueprint.h" #include "BuildingId.h" #include "BuildingType.h" #include "ItemType.h" @@ -38,3 +39,16 @@ struct BuildingConfig // by id, handling operational buildings and sites alike. Returns std::nullopt if // no such building or site exists. std::optional readBuildingConfig(const Simulation& sim, BuildingId id); + +// Captures a blueprint from a selection of building / construction-site ids, keeping +// only player-placeable buildings and recording each one's type, rotation, offset +// from the selection's bounding-box center, and configuration. Operational buildings +// and construction sites are treated identically (REQ-UI-BLUEPRINT-CREATE, +// REQ-UI-BLUEPRINT-STORAGE). The returned blueprint is unnamed. +Blueprint captureBlueprintFromSelection(const Simulation& sim, + const std::vector& selectedIds); + +// True if any selected id refers to a player-placeable building or construction site +// (the enable condition for the Create Blueprint button, REQ-UI-BLUEPRINT-CREATE). +bool selectionHasPlaceableBuilding(const Simulation& sim, + const std::vector& selectedIds); diff --git a/src/test/BlueprintTest.cpp b/src/test/BlueprintTest.cpp index 9fdb467..3f10df4 100644 --- a/src/test/BlueprintTest.cpp +++ b/src/test/BlueprintTest.cpp @@ -8,6 +8,7 @@ #include "Blueprint.h" #include "Building.h" +#include "BuildingConfig.h" #include "BuildingsConfig.h" #include "BuildingSystem.h" #include "BuildingType.h" @@ -696,6 +697,92 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction REQUIRE(b->recipeId == "mine_copper_ore"); } +// --------------------------------------------------------------------------- +// Blueprint capture from construction sites (REQ-UI-BLUEPRINT-CREATE, +// REQ-UI-BLUEPRINT-STORAGE): a still-under-construction building is a valid +// blueprint source, captured identically to an operational building. +// --------------------------------------------------------------------------- + +TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]") +{ + Simulation sim(loadConfig()); + + // Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps + // the body-cell bounding-box centered on the anchor, so a single site → zero offset. + const BuildingId id = + SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East); + REQUIRE(id != kInvalidBuildingId); + REQUIRE(sim.buildings().findSite(id) != nullptr); + REQUIRE(sim.buildings().findBuilding(id) == nullptr); + + const Blueprint bp = captureBlueprintFromSelection(sim, { id }); + + REQUIRE(bp.buildings.size() == 1); + REQUIRE(bp.buildings[0].type == BuildingType::Belt); + REQUIRE(bp.buildings[0].offset == QPoint(0, 0)); // single 1x1 building → zero offset +} + +TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]") +{ + Simulation sim(loadConfig()); + + const BuildingId id = + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(id != kInvalidBuildingId); + SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); + + const Blueprint bp = captureBlueprintFromSelection(sim, { id }); + + REQUIRE(bp.buildings.size() == 1); + REQUIRE(bp.buildings[0].recipeId == "mine_iron_ore"); +} + +TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured", + "[blueprint]") +{ + Simulation sim(loadConfig()); + + // Building A: place, configure, and tick to completion so it is operational. + const BuildingId idA = + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(idA != kInvalidBuildingId); + SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore"); + for (int i = 0; i <= static_cast(secondsToTicks(10.0)); ++i) { sim.tick(); } + REQUIRE(sim.buildings().findBuilding(idA) != nullptr); + + // Building B: place and configure, but leave as a construction site. + const BuildingId idB = + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East); + REQUIRE(idB != kInvalidBuildingId); + SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore"); + REQUIRE(sim.buildings().findSite(idB) != nullptr); + + const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB }); + + REQUIRE(bp.buildings.size() == 2); + REQUIRE(bp.buildings[0].type == BuildingType::Miner); + REQUIRE(bp.buildings[1].type == BuildingType::Miner); + // Both the operational building's and the site's configs come through. + std::vector recipes = { bp.buildings[0].recipeId, bp.buildings[1].recipeId }; + std::sort(recipes.begin(), recipes.end()); + REQUIRE(recipes == std::vector{ "mine_copper_ore", "mine_iron_ore" }); + // Distinct anchors → distinct offsets. + REQUIRE(bp.buildings[0].offset != bp.buildings[1].offset); +} + +TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]") +{ + Simulation sim(loadConfig()); + + REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {})); + + const BuildingId id = + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(id != kInvalidBuildingId); + REQUIRE(sim.buildings().findSite(id) != nullptr); + REQUIRE(selectionHasPlaceableBuilding(sim, { id })); +} + TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]") { // "interceptor" has unlock_at_station_level = -1 in the test config. diff --git a/src/ui/BlueprintPanel.cpp b/src/ui/BlueprintPanel.cpp index 2f71331..ea2753a 100644 --- a/src/ui/BlueprintPanel.cpp +++ b/src/ui/BlueprintPanel.cpp @@ -146,60 +146,10 @@ void BlueprintPanel::onBlueprintButtonClicked(int index) Blueprint BlueprintPanel::createBlueprintFromSelection() const { - struct Entry - { - const Building* building; - }; - std::vector entries; - entries.reserve(m_selectedBuildingIds.size()); - - for (const BuildingId id : m_selectedBuildingIds) - { - const Building* b = m_sim->buildings().findBuilding(id); - if (!b) { continue; } - const BuildingDef* def = m_config->buildings.findBuildingDef(b->type); - if (def && def->playerPlaceable) { entries.push_back({ b }); } - } - - if (entries.empty()) { return Blueprint{}; } - - int minX = INT_MAX, maxX = INT_MIN; - int minY = INT_MAX, maxY = INT_MIN; - for (const Entry& e : entries) - { - for (const QPoint& cell : e.building->bodyCells) - { - minX = std::min(minX, cell.x()); - maxX = std::max(maxX, cell.x()); - minY = std::min(minY, cell.y()); - maxY = std::max(maxY, cell.y()); - } - } - - const QPoint center((minX + maxX) / 2, (minY + maxY) / 2); - - Blueprint bp; - bp.buildings.reserve(entries.size()); - for (const Entry& e : entries) - { - BlueprintBuilding bb; - bb.type = e.building->type; - bb.rotation = e.building->rotation; - bb.offset = e.building->anchor - center; - // Recipe / schematic / layout / splitter-filter capture is shared with the - // copy-settings gesture (REQ-BLD-COPY-CONFIG) via readBuildingConfig. - const std::optional config = - readBuildingConfig(*m_sim, e.building->id); - if (config.has_value()) - { - bb.recipeId = config->recipeId.value_or(std::string()); - bb.shipLayout = config->shipLayout; - bb.splitterFilterA = config->splitterFilterA; - bb.splitterFilterB = config->splitterFilterB; - } - bp.buildings.push_back(bb); - } - return bp; + // Capture is shared, testable logic in lib/sim: it resolves each selected id as an + // operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE, + // REQ-UI-BLUEPRINT-STORAGE). + return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds); } int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const @@ -295,19 +245,8 @@ void BlueprintPanel::loadFromDisk() void BlueprintPanel::refreshButtonStates() { - const bool anyPlaceable = [&]() { - for (const BuildingId id : m_selectedBuildingIds) - { - const Building* b = m_sim->buildings().findBuilding(id); - if (!b) { continue; } - for (const BuildingDef& def : m_config->buildings.buildings) - { - if (def.type == b->type) { return def.playerPlaceable; } - } - } - return false; - }(); - m_createBtn->setEnabled(anyPlaceable); + // A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE). + m_createBtn->setEnabled(selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds)); for (int i = 0; i < static_cast(m_blueprintButtons.size()); ++i) {