diff --git a/src/lib/core/SelectionBox.h b/src/lib/core/SelectionBox.h index 12f2e74..34a57de 100644 --- a/src/lib/core/SelectionBox.h +++ b/src/lib/core/SelectionBox.h @@ -15,14 +15,17 @@ // Whether the box overlaps the unit square of `tile` — the rule for anything that // occupies whole tiles (buildings, construction sites, defence station bodies). The -// comparisons are inclusive, so a box that only grazes the tile's edge still covers -// it, and a box with no area covers the tile it lies on. +// overlap must have area: a box that ends exactly on a tile boundary does not reach +// into the tile beyond it. That matters because a click resolves as the unit square +// of the tile it hit (GameWorldView::getBoxWorldRect), so its edges lie exactly on +// tile boundaries — with the touching edges counted as coverage, such a box would +// cover the whole 3x3 neighbourhood instead of the one tile clicked. inline bool boxCoversTile(const QRectF& worldBox, QPoint tile) { - return worldBox.left() <= static_cast(tile.x()) + 1.0 - && worldBox.right() >= static_cast(tile.x()) - && worldBox.top() <= static_cast(tile.y()) + 1.0 - && worldBox.bottom() >= static_cast(tile.y()); + return worldBox.left() < static_cast(tile.x()) + 1.0 + && worldBox.right() > static_cast(tile.x()) + && worldBox.top() < static_cast(tile.y()) + 1.0 + && worldBox.bottom() > static_cast(tile.y()); } // Whether the box contains `worldPos` — the rule for anything that has a position diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index d64dbcd..357e6b5 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -168,6 +168,29 @@ TEST_CASE("buildingsInBox covers a body cell the box only reaches into", "[build REQUIRE(buildingsInBox(f.state, QRectF(1.2, 1.2, 0.5, 0.5)).empty()); } +TEST_CASE("buildingsInBox stops at the edge of a tile-aligned box", "[building]") +{ + PlacementFixture f; + + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), + Rotation::East, 0).value(); + + // Body at (0,0),(1,0),(0,1). A click resolves as the unit square of the tile it + // hit (GameWorldView::getBoxWorldRect), so the box edges land exactly on tile + // boundaries -- the case where an over-inclusive coverage rule reaches into the + // neighbouring tiles (REQ-UI-MULTI-SELECT). + const std::vector onBody = + buildingsInBox(f.state, QRectF(1.0, 0.0, 1.0, 1.0)); + REQUIRE(onBody.size() == 1); + REQUIRE(onBody.front() == id); + + // (2,0) is empty and merely touches the body cell (1,0) along their shared edge; + // clicking it must select nothing. Same for (1,1), which touches (1,0) and (0,1) + // along an edge each and (0,0) at a corner. + REQUIRE(buildingsInBox(f.state, QRectF(2.0, 0.0, 1.0, 1.0)).empty()); + REQUIRE(buildingsInBox(f.state, QRectF(1.0, 1.0, 1.0, 1.0)).empty()); +} + // -- World-bounds rejection (REQ-BLD-PLACE-VALID) --------------------------- TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[building]")