fix blueprint rotation bug
This commit is contained in:
@@ -7,12 +7,14 @@
|
||||
#include <QPoint>
|
||||
|
||||
#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<BuildingSpec>& 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user