target single-building transfers by hovering, not by footprint coincidence
Coincidence required the ghost's anchor to land exactly on the target's anchor, so on a 2x2 assembler three of its four tiles read as an ordinary overlap and went red: you had to find the top-left tile before the copy gesture would fire. A single-building blueprint now finds its target by hit-testing the cursor instead -- any body tile, any facing -- and its ghost snaps onto the target so it shows what the click will act on. A constellation is unchanged: it is placed as a layout, so its ghosts stay where the blueprint puts them and still need coincidence plus a matching rotation. The old "single-building blueprints ignore target rotation" carve-out is gone, subsumed by hovering. blueprintHoldsOneBuilding becomes std::optional<QPoint> hoverTile, which carries both facts in one parameter: set means "single-building blueprint, cursor here". BlueprintGhostResolved gains ghostAnchor/ghostRotation so the snap comes out of the classifier both callers already share, rather than the renderer working it out separately. Note the shipyard, which I had cited as the motivating case, is not one: its mask needs A cells on asteroid and S cells in space, and the asteroid edge is vertical, so a rotated shipyard can never be placed at all. Rotation was never the real problem here; alignment was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user