diff --git a/src/test/BlueprintTest.cpp b/src/test/BlueprintTest.cpp index c6bf326..61927ba 100644 --- a/src/test/BlueprintTest.cpp +++ b/src/test/BlueprintTest.cpp @@ -143,6 +143,28 @@ static GameConfig loadConfig() return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR); } +// Mirrors BlueprintPanel::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& specs, + const GameConfig& cfg) +{ + std::vector filtered; + for (const BuildingSpec& s : specs) + { + for (const BuildingDef& def : cfg.buildings.buildings) + { + if (def.type == s.type) + { + if (def.playerPlaceable) { filtered.push_back(s); } + break; + } + } + // If the type is not in buildings (e.g. Hq, defence stations), it is skipped. + } + return buildBlueprint(filtered); +} + // --------------------------------------------------------------------------- // Offset computation // --------------------------------------------------------------------------- @@ -435,6 +457,51 @@ TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[ REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(1, 0)); } +// --------------------------------------------------------------------------- +// Player-placeable filter in blueprint creation +// --------------------------------------------------------------------------- + +TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint", + "[blueprint]") +{ + const GameConfig cfg = loadConfig(); + + // Hq has no entry in buildings.toml, so it is treated as non-player-placeable. + const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East }; + const Blueprint bp = buildBlueprintFiltered({ hq }, cfg); + + REQUIRE(bp.buildings.empty()); +} + +TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings", + "[blueprint]") +{ + const GameConfig cfg = loadConfig(); + + const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East }; + const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East }; + const Blueprint bp = buildBlueprintFiltered({ belt, hq }, cfg); + + REQUIRE(bp.buildings.size() == 1); + REQUIRE(bp.buildings[0].type == BuildingType::Belt); +} + +TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings", + "[blueprint]") +{ + const GameConfig cfg = loadConfig(); + + // Belt at (-5, 0). HQ at (-3, 0) — excluded from the blueprint. + // If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1. + // With HQ excluded: bboxX = [-5, -5], center.x = -5, belt offset = 0. + const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East }; + const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East }; + const Blueprint bp = buildBlueprintFiltered({ belt, hq }, cfg); + + REQUIRE(bp.buildings.size() == 1); + REQUIRE(bp.buildings[0].offset == QPoint(0, 0)); +} + // --------------------------------------------------------------------------- // Simulation-level: blueprint placement places buildings at correct tiles // --------------------------------------------------------------------------- diff --git a/src/ui/BlueprintPanel.cpp b/src/ui/BlueprintPanel.cpp index 6f8413a..dd5aa13 100644 --- a/src/ui/BlueprintPanel.cpp +++ b/src/ui/BlueprintPanel.cpp @@ -152,7 +152,15 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const for (const EntityId id : m_selectedIds) { const Building* b = m_sim->buildings().findBuilding(id); - if (b) { entries.push_back({ b }); } + if (!b) { continue; } + const bool placeable = [&]() { + for (const BuildingDef& def : m_config->buildings.buildings) + { + if (def.type == b->type) { return def.playerPlaceable; } + } + return false; + }(); + if (placeable) { entries.push_back({ b }); } } if (entries.empty()) { return Blueprint{}; } @@ -235,7 +243,19 @@ void BlueprintPanel::rebuildButtons() void BlueprintPanel::refreshButtonStates() { - m_createBtn->setEnabled(!m_selectedIds.empty()); + const bool anyPlaceable = [&]() { + for (const EntityId id : m_selectedIds) + { + 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); for (int i = 0; i < static_cast(m_blueprintButtons.size()); ++i) {