stop a click selecting the tiles around the one it hit

Clicking empty ground selected every building in the 3x3 neighbourhood, and a
deconstruct click queued all of them -- demolishing any construction site among
them outright, since sites skip the queue.

Two allowances stacked. A click that hits nothing resolves as a box drag, and a
box that never moved becomes the unit square of the tile the button went down on
(GameWorldView::getBoxWorldRect), so its edges lie exactly on tile boundaries.
boxCoversTile then counted a shared boundary as coverage -- written for the
sub-tile drag rectangles it was introduced for, where "grazes the edge" is the
generous reading, but the click box grazes all four of its neighbours, and their
diagonals through the corners.

Coverage now has to have area: the far-edge comparisons are strict, so a box
ending on a boundary does not reach past it. Drags stop grabbing the one-tile
ring around themselves too, and station bodies come along, sharing the rule
through actorsInBox.

The existing box tests use rectangles that sit inside a tile, which is why none
of them saw this; the new one queries tile-aligned boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-20 11:24:28 +02:00
parent 75c049fa67
commit 613928b319
2 changed files with 32 additions and 6 deletions

View File

@@ -15,14 +15,17 @@
// Whether the box overlaps the unit square of `tile` — the rule for anything that // Whether the box overlaps the unit square of `tile` — the rule for anything that
// occupies whole tiles (buildings, construction sites, defence station bodies). The // 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 // overlap must have area: a box that ends exactly on a tile boundary does not reach
// it, and a box with no area covers the tile it lies on. // 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) inline bool boxCoversTile(const QRectF& worldBox, QPoint tile)
{ {
return worldBox.left() <= static_cast<qreal>(tile.x()) + 1.0 return worldBox.left() < static_cast<qreal>(tile.x()) + 1.0
&& worldBox.right() >= static_cast<qreal>(tile.x()) && worldBox.right() > static_cast<qreal>(tile.x())
&& worldBox.top() <= static_cast<qreal>(tile.y()) + 1.0 && worldBox.top() < static_cast<qreal>(tile.y()) + 1.0
&& worldBox.bottom() >= static_cast<qreal>(tile.y()); && worldBox.bottom() > static_cast<qreal>(tile.y());
} }
// Whether the box contains `worldPos` — the rule for anything that has a position // Whether the box contains `worldPos` — the rule for anything that has a position

View File

@@ -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()); 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<BuildingId> 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) --------------------------- // -- World-bounds rejection (REQ-BLD-PLACE-VALID) ---------------------------
TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[building]") TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[building]")