let any blueprint transfer configuration, not just single-building ones
Implements 0593f48. A coinciding building of a configurable type now takes
the blueprint's settings whatever the blueprint's size, so dropping a
constellation over a partial copy of itself configures what is already
standing there instead of leaving it blank.
The whole behaviour change is one branch in resolveBlueprintGhost: at a
matching rotation, a coinciding building transfers if its type has settings
and is a compatible overlap otherwise; at a differing rotation only a
single-building blueprint still transfers. GameWorldView and WorldRenderer
needed nothing -- they already switch on the action, already exclude
transfers from the cost, and already tint them.
blueprintHoldsOneBuilding survives only as that rotation carve-out, and the
header now says so.
Note for anyone testing this by hand: the carve-out is narrower than it
reads. It is only reachable for footprints that survive rotation -- 1x1
bodies like the splitter, or fully filled symmetric ones. A miner's body is
L-shaped ("AA" / "A>"), so a rotated miner ghost covers different tiles and
coincides with nothing at all. That is the coincidence test doing its job,
not these rules, but it cost one wrong test before it was noticed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -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};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]")
|
||||
|
||||
Reference in New Issue
Block a user