re-cover copy-settings through single-building blueprints

This commit is contained in:
2026-08-06 19:44:55 +02:00
parent 9a3b6c10d6
commit 08d8b0dd90
13 changed files with 542 additions and 65 deletions

View File

@@ -1039,6 +1039,206 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
REQUIRE(*result == id);
}
// ---------------------------------------------------------------------------
// resolveBlueprintGhost
// ---------------------------------------------------------------------------
// What a blueprint ghost does where it meets an existing building
// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). Blueprint placement never
// rotates anything, so a coinciding building must either take the blueprint's settings
// or already match it exactly.
namespace
{
BlueprintGhostResolved resolveOne(const PlacementFixture& f, BuildingType type,
QPoint anchor, Rotation rotation)
{
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation,
/*blueprintHoldsOneBuilding*/ true);
}
BlueprintGhostResolved resolveInConstellation(const PlacementFixture& f, BuildingType type,
QPoint anchor, Rotation rotation)
{
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation,
/*blueprintHoldsOneBuilding*/ false);
}
} // namespace
TEST_CASE("isConfigurableBuildingType: only types with player-facing settings",
"[blueprint]")
{
// The gate on whether a single-building blueprint transfers anything at all.
CHECK(isConfigurableBuildingType(BuildingType::Miner));
CHECK(isConfigurableBuildingType(BuildingType::Assembler));
CHECK(isConfigurableBuildingType(BuildingType::Shipyard));
CHECK(isConfigurableBuildingType(BuildingType::Splitter));
// Smelter and Reprocessing Plant run implicit recipes (REQ-BLD-SMELTER,
// REQ-BLD-REPROCESSING) and the rest have no settings whatsoever.
CHECK_FALSE(isConfigurableBuildingType(BuildingType::Smelter));
CHECK_FALSE(isConfigurableBuildingType(BuildingType::ReprocessingPlant));
CHECK_FALSE(isConfigurableBuildingType(BuildingType::SalvageBay));
CHECK_FALSE(isConfigurableBuildingType(BuildingType::Belt));
CHECK_FALSE(isConfigurableBuildingType(BuildingType::TunnelEntry));
CHECK_FALSE(isConfigurableBuildingType(BuildingType::TunnelExit));
CHECK_FALSE(isConfigurableBuildingType(BuildingType::Hq));
}
TEST_CASE("resolveBlueprintGhost: free valid cells place a new building", "[blueprint]")
{
PlacementFixture f;
// Anchored on the asteroid (x < 0): a miner is all-asteroid cells. BuildingSystem's
// place() skips the terrain rules, but resolveBlueprintGhost applies them.
const BlueprintGhostResolved resolved =
resolveOne(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
CHECK(resolved.action == BlueprintGhostAction::PlaceNew);
CHECK_FALSE(resolved.targetId.has_value());
}
TEST_CASE("resolveBlueprintGhost: terrain-invalid positions are invalid", "[blueprint]")
{
PlacementFixture f;
// A miner is all-asteroid (A) cells, so it cannot sit out in space (x >= 0).
CHECK(resolveOne(f, BuildingType::Miner, QPoint(5, 0), Rotation::East).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: overlapping a different building type is invalid",
"[blueprint]")
{
PlacementFixture f;
f.bs.place(f.state, BuildingType::Belt, QPoint(-1, 0), Rotation::East, 0);
CHECK(resolveOne(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::East).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: a partial overlap of the same type is invalid",
"[blueprint]")
{
PlacementFixture f;
// Smelter at (-3,0) covers (-3,0),(-2,0),(-3,1),(-2,1); a ghost at (-2,0) covers only
// two of those, so it coincides with nothing and is an ordinary occupied overlap.
// Both footprints stay on the asteroid, so terrain is not what fails here.
f.bs.place(f.state, BuildingType::Smelter, QPoint(-3, 0), Rotation::East, 0);
CHECK(resolveOne(f, BuildingType::Smelter, QPoint(-2, 0), Rotation::East).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: a single configurable building transfers its settings",
"[blueprint]")
{
PlacementFixture f;
const BuildingId id =
f.bs.place(f.state, BuildingType::Miner, QPoint(-2, 0), Rotation::East, 0).value();
const BlueprintGhostResolved resolved =
resolveOne(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
REQUIRE(resolved.targetId.has_value());
CHECK(*resolved.targetId == id);
}
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).
PlacementFixture f;
const BuildingId id =
f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0).value();
const BlueprintGhostResolved resolved =
resolveOne(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North);
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
REQUIRE(resolved.targetId.has_value());
CHECK(*resolved.targetId == id);
}
TEST_CASE("resolveBlueprintGhost: a construction site is a transfer target too",
"[blueprint]")
{
PlacementFixture f;
// Not ticked to completion, so it is still queued (REQ-BLD-SITE-CONFIG).
const BuildingId id =
f.bs.place(f.state, BuildingType::Assembler, QPoint(-3, 0), Rotation::East, 0).value();
REQUIRE_FALSE(getAllSites(f.state).empty());
const BlueprintGhostResolved resolved =
resolveOne(f, BuildingType::Assembler, QPoint(-3, 0), Rotation::East);
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
CHECK(*resolved.targetId == id);
}
TEST_CASE("resolveBlueprintGhost: a single building with no settings overlaps instead",
"[blueprint]")
{
// A belt carries nothing to transfer, so the same footprint is a compatible overlap
// when the facings match -- and invalid when they do not, since nothing here may
// re-orient it (REQ-UI-BLUEPRINT-OVERLAP).
PlacementFixture f;
const BuildingId id =
f.bs.place(f.state, BuildingType::Belt, QPoint(-1, 0), Rotation::East, 0).value();
const BlueprintGhostResolved matching =
resolveOne(f, BuildingType::Belt, QPoint(-1, 0), Rotation::East);
REQUIRE(matching.action == BlueprintGhostAction::CompatibleOverlap);
CHECK(*matching.targetId == id);
CHECK(resolveOne(f, BuildingType::Belt, QPoint(-1, 0), Rotation::North).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: nothing in a multi-building blueprint transfers",
"[blueprint]")
{
// Even a configurable building coinciding with its twin only overlaps once the
// blueprint holds more than one building (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 =
resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(matching.action == BlueprintGhostAction::CompatibleOverlap);
CHECK(*matching.targetId == id);
// ... and a differently-facing twin blocks the whole constellation.
CHECK(resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::North).action
== BlueprintGhostAction::Invalid);
}
TEST_CASE("resolveBlueprintGhost: an identical tunnel is a compatible overlap", "[blueprint]")
{
// findRotateInPlaceTarget refuses tunnels because re-orienting one is unsupported.
// Nothing is re-oriented here, so that reason does not apply and the tunnel the
// blueprint wants -- already there, same facing -- is simply left alone.
PlacementFixture f;
const BuildingId id =
f.bs.place(f.state, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East, 0).value();
f.bs.place(f.state, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0);
REQUIRE_FALSE(
findRotateInPlaceTarget(f.state, f.cfg, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East)
.has_value());
const BlueprintGhostResolved resolved =
resolveInConstellation(f, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East);
REQUIRE(resolved.action == BlueprintGhostAction::CompatibleOverlap);
CHECK(*resolved.targetId == id);
}
// ---------------------------------------------------------------------------
// rotateInPlace
// ---------------------------------------------------------------------------