From 7859b38d6287c5691897e9786a3d9dfd699dd2a3 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Sun, 26 Apr 2026 22:36:49 +0200 Subject: [PATCH] fix blueprint rotation bug --- src/test/BlueprintTest.cpp | 130 ++++++++++++++++++++++++++++++++++--- src/ui/GameWorldView.cpp | 25 ++++++- 2 files changed, 143 insertions(+), 12 deletions(-) diff --git a/src/test/BlueprintTest.cpp b/src/test/BlueprintTest.cpp index 9156e0c..c6bf326 100644 --- a/src/test/BlueprintTest.cpp +++ b/src/test/BlueprintTest.cpp @@ -7,12 +7,14 @@ #include #include "Blueprint.h" +#include "BuildingsConfig.h" #include "BuildingSystem.h" #include "BuildingType.h" #include "ConfigLoader.h" #include "EntityId.h" #include "Rotation.h" #include "Simulation.h" +#include "SurfaceMask.h" // --------------------------------------------------------------------------- // Helpers that mirror the production implementations under test. @@ -87,21 +89,51 @@ static Blueprint buildBlueprint(const std::vector& specs) } // Apply one CW/CCW rotation to every building in the constellation, mirroring -// the Q / E handling in GameWorldView::keyPressEvent. -static void applyRotationCW(Blueprint& bp) +// the corrected Q / E handling in GameWorldView::keyPressEvent. +// Each building's anchor is recomputed from the rotated body cells so that +// multi-tile footprints stay correctly aligned after rotation. +static void applyRotationCW(Blueprint& bp, const GameConfig& cfg) { for (BlueprintBuilding& bb : bp.buildings) { - bb.offset = rotateCW(bb.offset); + const BuildingDef* def = nullptr; + for (const BuildingDef& d : cfg.buildings.buildings) + { + if (d.type == bb.type) { def = &d; break; } + } + if (!def) { continue; } + const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); + int minX = INT_MAX, minY = INT_MAX; + for (const QPoint& cell : mask.bodyCells) + { + const QPoint abs = bb.offset + cell; + minX = std::min(minX, -abs.y()); + minY = std::min(minY, abs.x()); + } + bb.offset = QPoint(minX, minY); bb.rotation = rotCW(bb.rotation); } } -static void applyRotationCCW(Blueprint& bp) +static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg) { for (BlueprintBuilding& bb : bp.buildings) { - bb.offset = rotateCCW(bb.offset); + const BuildingDef* def = nullptr; + for (const BuildingDef& d : cfg.buildings.buildings) + { + if (d.type == bb.type) { def = &d; break; } + } + if (!def) { continue; } + const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); + int minX = INT_MAX, minY = INT_MAX; + for (const QPoint& cell : mask.bodyCells) + { + const QPoint abs = bb.offset + cell; + minX = std::min(minX, abs.y()); + minY = std::min(minY, -abs.x()); + } + bb.offset = QPoint(minX, minY); bb.rotation = rotCCW(bb.rotation); } } @@ -249,6 +281,7 @@ TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]") TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]") { + const GameConfig cfg = loadConfig(); // Building one tile to the right, facing East. Blueprint bp; bp.name = "test"; @@ -258,7 +291,7 @@ TEST_CASE("Blueprint: CW constellation rotation updates offset and building rota bb.offset = QPoint(1, 0); bp.buildings.push_back(bb); - applyRotationCW(bp); + applyRotationCW(bp, cfg); // Offset: right → down. REQUIRE(bp.buildings[0].offset == QPoint(0, 1)); @@ -268,6 +301,7 @@ TEST_CASE("Blueprint: CW constellation rotation updates offset and building rota TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]") { + const GameConfig cfg = loadConfig(); Blueprint bp; bp.name = "test"; BlueprintBuilding bb; @@ -276,7 +310,7 @@ TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rot bb.offset = QPoint(1, 0); bp.buildings.push_back(bb); - applyRotationCCW(bp); + applyRotationCCW(bp, cfg); // Offset: right → up. REQUIRE(bp.buildings[0].offset == QPoint(0, -1)); @@ -286,6 +320,7 @@ TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rot TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]") { + const GameConfig cfg = loadConfig(); Blueprint bp; bp.name = "test"; BlueprintBuilding bb; @@ -294,10 +329,10 @@ TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", " bb.offset = QPoint(2, -1); bp.buildings.push_back(bb); - const QPoint originalOffset = bb.offset; + const QPoint originalOffset = bb.offset; const Rotation originalRotation = bb.rotation; - for (int i = 0; i < 4; ++i) { applyRotationCW(bp); } + for (int i = 0; i < 4; ++i) { applyRotationCW(bp, cfg); } REQUIRE(bp.buildings[0].offset == originalOffset); REQUIRE(bp.buildings[0].rotation == originalRotation); @@ -305,6 +340,7 @@ TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", " TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]") { + const GameConfig cfg = loadConfig(); // Two buildings left and right of center; after CW they should be above and below. Blueprint bp; bp.name = "test"; @@ -315,7 +351,7 @@ TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[ right.offset = QPoint( 1, 0); bp.buildings = { left, right }; - applyRotationCW(bp); + applyRotationCW(bp, cfg); // left (-1, 0) → CW → (0, -1) (above center) // right ( 1, 0) → CW → (0, 1) (below center) @@ -325,6 +361,80 @@ TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[ REQUIRE(bp.buildings[1].rotation == Rotation::South); } +// --------------------------------------------------------------------------- +// Regression: multi-tile building rotation (the anchor-offset-only bug) +// --------------------------------------------------------------------------- + +TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]") +{ + const GameConfig cfg = loadConfig(); + + // East miner: anchor (0,0), body cells (0,0),(1,0),(0,1). + // Output port indicator '>' at (1,1) → port tile (1,1), direction East. + // Belt placed at (1,1) — the port exit tile. + // Constellation body cells: {(0,0),(1,0),(0,1),(1,1)}. + // Integer center: ((0+1)/2, (0+1)/2) = (0,0). + // Miner offset (0,0), belt offset (1,1). + Blueprint bp; + bp.name = "test"; + BlueprintBuilding miner; + miner.type = BuildingType::Miner; + miner.rotation = Rotation::East; + miner.offset = QPoint(0, 0); + BlueprintBuilding belt; + belt.type = BuildingType::Belt; + belt.rotation = Rotation::East; + belt.offset = QPoint(1, 1); // port exit tile of East miner relative to its anchor + bp.buildings = { miner, belt }; + + applyRotationCW(bp, cfg); + + // Miner body cells (0,0),(1,0),(0,1) rotated CW: (0,0),(0,1),(-1,0). + // New miner anchor = min(-1..0, 0..1) = (-1, 0). + REQUIRE(bp.buildings[0].offset == QPoint(-1, 0)); + REQUIRE(bp.buildings[0].rotation == Rotation::South); + + // Belt body cell (1,1) rotated CW: (-1, 1). New belt anchor = (-1, 1). + REQUIRE(bp.buildings[1].offset == QPoint(-1, 1)); + REQUIRE(bp.buildings[1].rotation == Rotation::South); + + // South miner port tile relative to its anchor is (0,1) (the 'v' indicator in ["AA","vA"]). + // Miner anchor in constellation = (-1,0). Port exit = (-1,0)+(0,1) = (-1,1) = belt anchor. ✓ + REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(0, 1)); +} + +TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]") +{ + const GameConfig cfg = loadConfig(); + + Blueprint bp; + bp.name = "test"; + BlueprintBuilding miner; + miner.type = BuildingType::Miner; + miner.rotation = Rotation::East; + miner.offset = QPoint(0, 0); + BlueprintBuilding belt; + belt.type = BuildingType::Belt; + belt.rotation = Rotation::East; + belt.offset = QPoint(1, 1); + bp.buildings = { miner, belt }; + + applyRotationCCW(bp, cfg); + + // Miner body cells (0,0),(1,0),(0,1) rotated CCW: (0,0),(0,-1),(1,0). + // New miner anchor = min(0..1, -1..0) = (0, -1). + REQUIRE(bp.buildings[0].offset == QPoint(0, -1)); + REQUIRE(bp.buildings[0].rotation == Rotation::North); + + // Belt body cell (1,1) rotated CCW: (1,-1). New belt anchor = (1,-1). + REQUIRE(bp.buildings[1].offset == QPoint(1, -1)); + REQUIRE(bp.buildings[1].rotation == Rotation::North); + + // North miner port tile relative to its anchor is (1,0) (the '^' indicator in ["A^","AA"]). + // Miner anchor in constellation = (0,-1). Port exit = (0,-1)+(1,0) = (1,-1) = belt anchor. ✓ + REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(1, 0)); +} + // --------------------------------------------------------------------------- // Simulation-level: blueprint placement places buildings at correct tiles // --------------------------------------------------------------------------- diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 5b7c922..8a028e7 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -936,7 +937,17 @@ void GameWorldView::keyPressEvent(QKeyEvent* event) { for (BlueprintBuilding& bb : m_blueprintMode->buildings) { - bb.offset = QPoint(-bb.offset.y(), bb.offset.x()); + const BuildingDef* def = findBuildingDef(bb.type); + if (!def) { continue; } + const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); + int minX = INT_MAX, minY = INT_MAX; + for (const QPoint& cell : mask.bodyCells) + { + const QPoint abs = bb.offset + cell; + minX = std::min(minX, -abs.y()); + minY = std::min(minY, abs.x()); + } + bb.offset = QPoint(minX, minY); bb.rotation = rotateClockwise(bb.rotation); } } @@ -951,7 +962,17 @@ void GameWorldView::keyPressEvent(QKeyEvent* event) { for (BlueprintBuilding& bb : m_blueprintMode->buildings) { - bb.offset = QPoint(bb.offset.y(), -bb.offset.x()); + const BuildingDef* def = findBuildingDef(bb.type); + if (!def) { continue; } + const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); + int minX = INT_MAX, minY = INT_MAX; + for (const QPoint& cell : mask.bodyCells) + { + const QPoint abs = bb.offset + cell; + minX = std::min(minX, abs.y()); + minY = std::min(minY, -abs.x()); + } + bb.offset = QPoint(minX, minY); bb.rotation = rotateCounterClockwise(bb.rotation); } }