allow to rotate buildings in place

This commit is contained in:
2026-04-29 21:40:00 +02:00
parent 7e0104e9b8
commit 2770bf96be
4 changed files with 395 additions and 7 deletions

View File

@@ -535,3 +535,245 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
// No new production: inputs were consumed and not replenished.
REQUIRE_FALSE(b->production.has_value());
}
// ---------------------------------------------------------------------------
// findRotateInPlaceTarget
// ---------------------------------------------------------------------------
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is empty",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::East).has_value());
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const std::optional<EntityId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::North);
REQUIRE(result.has_value());
REQUIRE(*result == id);
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a completed operational belt",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(bs.allSites().empty());
const std::optional<EntityId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::South);
REQUIRE(result.has_value());
REQUIRE(*result == id);
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building type differs",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
// Querying with Splitter at the same tile — type mismatch → nullopt.
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value());
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
// Smelter at (0,0) occupies body tiles (0,0),(1,0),(0,1),(1,1).
bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
// Ghost anchored at (1,0) would cover (1,0),(2,0),(1,1),(2,1):
// only (1,0) and (1,1) are occupied — not a full coincidence.
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value());
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
// Smelter is a fully filled 2×2 footprint — rotating the ghost produces the
// same four body tiles, so findRotateInPlaceTarget must still return the id.
const EntityId id = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
const std::optional<EntityId> result =
bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(0, 0), Rotation::North);
REQUIRE(result.has_value());
REQUIRE(*result == id);
}
// ---------------------------------------------------------------------------
// rotateInPlace
// ---------------------------------------------------------------------------
TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a construction site",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
REQUIRE(bs.findSite(id)->rotation == Rotation::East);
bs.rotateInPlace(id, Rotation::North);
REQUIRE(bs.findSite(id)->rotation == Rotation::North);
}
TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of a queued site",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const Tick completesAt = bs.findSite(id)->completesAt;
REQUIRE(completesAt > 0);
bs.rotateInPlace(id, Rotation::South);
REQUIRE(bs.findSite(id)->completesAt == completesAt);
}
TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direction on an operational building",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(bs.findBuilding(id) != nullptr);
const Building& before = *bs.findBuilding(id);
REQUIRE(before.outputPorts[0].direction == Rotation::East);
bs.rotateInPlace(id, Rotation::North);
const Building& after = *bs.findBuilding(id);
REQUIRE(after.rotation == Rotation::North);
REQUIRE(after.outputPorts[0].direction == Rotation::North);
}
TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSystem so it still accepts items",
"[building][rotate-in-place]")
{
const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
int stock = 0;
std::mt19937 rng(0);
EntityId nextId = 1;
BuildingSystem bs(cfg, belts,
[&nextId]() { return nextId++; },
[&stock](int n) { stock += n; },
[](const std::string&, QVector2D) {},
rng);
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
bs.rotateInPlace(id, Rotation::North);
// Belt tile must still be registered after rotation — items can be placed on it.
REQUIRE(belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore")));
}