let any blueprint transfer configuration, not just single-building ones (if the orientation matches)

This commit is contained in:
2026-08-06 20:29:20 +02:00
parent 08d8b0dd90
commit 98deab932a
4 changed files with 93 additions and 36 deletions

View File

@@ -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};
}

View File

@@ -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.

View File

@@ -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]")