diff --git a/src/lib/sim/PlacementRules.cpp b/src/lib/sim/PlacementRules.cpp index 97e54bc..28ace96 100644 --- a/src/lib/sim/PlacementRules.cpp +++ b/src/lib/sim/PlacementRules.cpp @@ -144,24 +144,29 @@ BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state, const Ga findCoincidingSameTypeBuilding(state, config, type, anchor, rotation); if (coinciding.has_value()) { - // A single configurable building hands its settings over, whatever way the target - // faces: a transfer never rotates anything, so the target's facing does not matter - // (REQ-UI-BLUEPRINT-TRANSFER). Tested before the overlap rule, which then governs - // only what this does not claim. - if (blueprintHoldsOneBuilding && isConfigurableBuildingType(type)) - { - return BlueprintGhostResolved{BlueprintGhostAction::Transfer, coinciding->id}; - } + const bool configurable = isConfigurableBuildingType(type); - // Otherwise the building the blueprint wants must already be there in full, - // facing the same way, because nothing here may re-orient it - // (REQ-UI-BLUEPRINT-OVERLAP). Tunnels are not excluded: nothing is rotated, so - // the reason for their REQ-BLD-ROTATE-IN-PLACE exception does not arise. + // The building the blueprint wants is already there, facing the same way. It + // takes the blueprint's settings if it has any to take (REQ-UI-BLUEPRINT-TRANSFER) + // and is otherwise left exactly as it is (REQ-UI-BLUEPRINT-OVERLAP). Tunnels are + // not excluded from the latter: nothing is rotated, so the reason for their + // REQ-BLD-ROTATE-IN-PLACE exception does not arise. if (coinciding->rotation == rotation) { - return BlueprintGhostResolved{BlueprintGhostAction::CompatibleOverlap, + return BlueprintGhostResolved{configurable + ? BlueprintGhostAction::Transfer + : BlueprintGhostAction::CompatibleOverlap, coinciding->id}; } + + // Facing the other way. Placement may not re-orient it, so this is invalid -- + // except for the deliberate one-click gesture of a single-building blueprint, + // where a transfer rotates nothing anyway and the target's facing is beside the + // point (REQ-UI-BLUEPRINT-TRANSFER). + if (blueprintHoldsOneBuilding && configurable) + { + return BlueprintGhostResolved{BlueprintGhostAction::Transfer, coinciding->id}; + } return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt}; } diff --git a/src/lib/sim/PlacementRules.h b/src/lib/sim/PlacementRules.h index a8a5c5a..085ed94 100644 --- a/src/lib/sim/PlacementRules.h +++ b/src/lib/sim/PlacementRules.h @@ -82,9 +82,11 @@ struct BlueprintGhostResolved }; // Classifies one ghost of a blueprint against the current factory state -// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). `blueprintHoldsOneBuilding` is -// the blueprint's stored size, counted before locked types are dropped, so the gesture -// does not change behavior as the player unlocks things. +// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). `blueprintHoldsOneBuilding` +// only widens the transfer rule to differently-facing targets; a same-facing target +// transfers whatever the blueprint's size. It is the blueprint's stored size, counted +// before locked types are dropped, so the gesture does not change behavior as the player +// unlocks things. // // Shared by the ghost coloring and the click path so a preview cannot disagree with what // the click then does -- the same reason resolveBeltDragPath is shared. diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index d7c7dc8..e13d898 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -1198,24 +1198,72 @@ TEST_CASE("resolveBlueprintGhost: a single building with no settings overlaps in == BlueprintGhostAction::Invalid); } -TEST_CASE("resolveBlueprintGhost: nothing in a multi-building blueprint transfers", +TEST_CASE("resolveBlueprintGhost: a constellation transfers onto a matching building", "[blueprint]") { - // Even a configurable building coinciding with its twin only overlaps once the - // blueprint holds more than one building (REQ-UI-BLUEPRINT-TRANSFER). + // Blueprint size does not gate the transfer itself: a configurable building already + // standing where the blueprint wants it, facing the same way, takes its settings + // whatever else the blueprint holds (REQ-UI-BLUEPRINT-TRANSFER). PlacementFixture f; const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(-2, 0), Rotation::East, 0).value(); - const BlueprintGhostResolved matching = + const BlueprintGhostResolved resolved = resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East); - REQUIRE(matching.action == BlueprintGhostAction::CompatibleOverlap); - CHECK(*matching.targetId == id); + REQUIRE(resolved.action == BlueprintGhostAction::Transfer); + CHECK(*resolved.targetId == id); +} - // ... and a differently-facing twin blocks the whole constellation. - CHECK(resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::North).action +TEST_CASE("resolveBlueprintGhost: only a single-building blueprint ignores target 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. + PlacementFixture f; + + const BuildingId id = + f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0).value(); + + 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", + "[blueprint]") +{ + // One drop can reconfigure some of the buildings already there while leaving others + // alone: the split is by whether the type has settings at all, not by blueprint size + // (REQ-UI-BLUEPRINT-OVERLAP). + PlacementFixture f; + + const BuildingId minerId = + f.bs.place(f.state, BuildingType::Miner, QPoint(-2, 0), Rotation::East, 0).value(); + const BuildingId smelterId = + f.bs.place(f.state, BuildingType::Smelter, QPoint(-5, 0), Rotation::East, 0).value(); + + const BlueprintGhostResolved miner = + resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(miner.action == BlueprintGhostAction::Transfer); + CHECK(*miner.targetId == minerId); + + // A smelter runs an implicit recipe (REQ-BLD-SMELTER), so there is nothing to hand + // over and it is simply left as it is. + const BlueprintGhostResolved smelter = + resolveInConstellation(f, BuildingType::Smelter, QPoint(-5, 0), Rotation::East); + REQUIRE(smelter.action == BlueprintGhostAction::CompatibleOverlap); + CHECK(*smelter.targetId == smelterId); } TEST_CASE("resolveBlueprintGhost: an identical tunnel is a compatible overlap", "[blueprint]")