target single-building transfers by hovering, not by footprint coincidence

This commit is contained in:
2026-08-06 20:31:51 +02:00
parent 98deab932a
commit 3b37b0ecf8
6 changed files with 171 additions and 68 deletions

View File

@@ -1050,18 +1050,25 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
namespace
{
// A single-building blueprint, whose cursor sits on the ghost's own anchor unless a test
// says otherwise. That gesture hit-tests the cursor for its transfer target.
BlueprintGhostResolved resolveOne(const PlacementFixture& f, BuildingType type,
QPoint anchor, Rotation rotation)
{
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation,
/*blueprintHoldsOneBuilding*/ true);
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation, anchor);
}
BlueprintGhostResolved resolveOneHovering(const PlacementFixture& f, BuildingType type,
QPoint anchor, Rotation rotation, QPoint cursorTile)
{
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation, cursorTile);
}
// One ghost of a constellation: no cursor hit-test, judged purely on where it sits.
BlueprintGhostResolved resolveInConstellation(const PlacementFixture& f, BuildingType type,
QPoint anchor, Rotation rotation)
{
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation,
/*blueprintHoldsOneBuilding*/ false);
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation, std::nullopt);
}
} // namespace
@@ -1148,8 +1155,9 @@ TEST_CASE("resolveBlueprintGhost: a single configurable building transfers its s
TEST_CASE("resolveBlueprintGhost: a transfer ignores the target's rotation", "[blueprint]")
{
// The rule transfer does not share with compatible overlap: a transfer never rotates
// anything, so which way the target faces cannot matter (REQ-UI-BLUEPRINT-TRANSFER).
// A transfer never rotates anything, so which way the target faces cannot matter
// (REQ-UI-BLUEPRINT-TRANSFER). The ghost snaps to the target's facing rather than
// keeping the blueprint's.
PlacementFixture f;
const BuildingId id =
@@ -1160,6 +1168,47 @@ TEST_CASE("resolveBlueprintGhost: a transfer ignores the target's rotation", "[b
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
REQUIRE(resolved.targetId.has_value());
CHECK(*resolved.targetId == id);
CHECK(resolved.ghostRotation == Rotation::East);
}
TEST_CASE("resolveBlueprintGhost: a single-building blueprint transfers from anywhere on the target",
"[blueprint]")
{
// The point of hit-testing the cursor instead of comparing footprints. Coincidence
// needs the ghost's anchor to land on the target's own anchor, so with a 2x2 body
// three of its four tiles missed and read as an ordinary overlap. Hovering any body
// tile now targets it, and the ghost snaps onto the building
// (REQ-UI-BLUEPRINT-TRANSFER).
PlacementFixture f;
// Assembler body covers (-3,0),(-2,0),(-3,1),(-2,1); its anchor is (-3,0).
const BuildingId id =
f.bs.place(f.state, BuildingType::Assembler, QPoint(-3, 0), Rotation::East, 0).value();
const QPoint offAnchorTile(-2, 1);
const BlueprintGhostResolved resolved =
resolveOneHovering(f, BuildingType::Assembler, offAnchorTile, Rotation::East,
offAnchorTile);
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
CHECK(*resolved.targetId == id);
CHECK(resolved.ghostAnchor == QPoint(-3, 0));
// The same misaligned ghost inside a constellation still just overlaps invalidly:
// a layout is placed where the blueprint puts it, and nothing snaps.
CHECK(resolveInConstellation(f, BuildingType::Assembler, offAnchorTile, Rotation::East).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: hovering a different building type does not transfer",
"[blueprint]")
{
PlacementFixture f;
f.bs.place(f.state, BuildingType::Smelter, QPoint(-3, 0), Rotation::East, 0);
// A miner blueprint over a smelter: the cursor hit-test only matches its own type.
CHECK(resolveOne(f, BuildingType::Miner, QPoint(-3, 0), Rotation::East).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: a construction site is a transfer target too",
@@ -1215,29 +1264,18 @@ TEST_CASE("resolveBlueprintGhost: a constellation transfers onto a matching buil
CHECK(*resolved.targetId == id);
}
TEST_CASE("resolveBlueprintGhost: only a single-building blueprint ignores target rotation",
TEST_CASE("resolveBlueprintGhost: a constellation still requires a matching rotation",
"[blueprint]")
{
// The one thing blueprint size still decides. Inside a constellation a differently
// facing twin cannot be re-oriented, so it blocks the whole placement; alone, the
// gesture transfers anyway because it rotates nothing (REQ-UI-BLUEPRINT-TRANSFER).
//
// A splitter, because the question only arises for a footprint that survives
// rotation. A miner's body is L-shaped ("AA" / "A>"), so a rotated miner ghost covers
// different tiles and coincides with nothing at all -- invalid for a reason that has
// nothing to do with these rules.
// Only the single-building gesture is forgiving about facing. A constellation's
// ghosts stay where the blueprint puts them, and one that cannot be re-oriented to
// match blocks the whole placement (REQ-UI-BLUEPRINT-OVERLAP).
PlacementFixture f;
const BuildingId id =
f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0).value();
f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0);
CHECK(resolveInConstellation(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North).action
== BlueprintGhostAction::Invalid);
const BlueprintGhostResolved alone =
resolveOne(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North);
REQUIRE(alone.action == BlueprintGhostAction::Transfer);
CHECK(*alone.targetId == id);
}
TEST_CASE("resolveBlueprintGhost: a constellation mixes transfers and plain overlaps",