fix blueprint rotation bug
This commit is contained in:
@@ -7,12 +7,14 @@
|
|||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
|
||||||
#include "Blueprint.h"
|
#include "Blueprint.h"
|
||||||
|
#include "BuildingsConfig.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "EntityId.h"
|
#include "EntityId.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers that mirror the production implementations under test.
|
// 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
|
// Apply one CW/CCW rotation to every building in the constellation, mirroring
|
||||||
// the Q / E handling in GameWorldView::keyPressEvent.
|
// the corrected Q / E handling in GameWorldView::keyPressEvent.
|
||||||
static void applyRotationCW(Blueprint& bp)
|
// 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)
|
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);
|
bb.rotation = rotCW(bb.rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void applyRotationCCW(Blueprint& bp)
|
static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
|
||||||
{
|
{
|
||||||
for (BlueprintBuilding& bb : bp.buildings)
|
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);
|
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]")
|
TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||||
{
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
// Building one tile to the right, facing East.
|
// Building one tile to the right, facing East.
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
@@ -258,7 +291,7 @@ TEST_CASE("Blueprint: CW constellation rotation updates offset and building rota
|
|||||||
bb.offset = QPoint(1, 0);
|
bb.offset = QPoint(1, 0);
|
||||||
bp.buildings.push_back(bb);
|
bp.buildings.push_back(bb);
|
||||||
|
|
||||||
applyRotationCW(bp);
|
applyRotationCW(bp, cfg);
|
||||||
|
|
||||||
// Offset: right → down.
|
// Offset: right → down.
|
||||||
REQUIRE(bp.buildings[0].offset == QPoint(0, 1));
|
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]")
|
TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||||
{
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
BlueprintBuilding bb;
|
BlueprintBuilding bb;
|
||||||
@@ -276,7 +310,7 @@ TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rot
|
|||||||
bb.offset = QPoint(1, 0);
|
bb.offset = QPoint(1, 0);
|
||||||
bp.buildings.push_back(bb);
|
bp.buildings.push_back(bb);
|
||||||
|
|
||||||
applyRotationCCW(bp);
|
applyRotationCCW(bp, cfg);
|
||||||
|
|
||||||
// Offset: right → up.
|
// Offset: right → up.
|
||||||
REQUIRE(bp.buildings[0].offset == QPoint(0, -1));
|
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]")
|
TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
|
||||||
{
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
BlueprintBuilding bb;
|
BlueprintBuilding bb;
|
||||||
@@ -294,10 +329,10 @@ TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "
|
|||||||
bb.offset = QPoint(2, -1);
|
bb.offset = QPoint(2, -1);
|
||||||
bp.buildings.push_back(bb);
|
bp.buildings.push_back(bb);
|
||||||
|
|
||||||
const QPoint originalOffset = bb.offset;
|
const QPoint originalOffset = bb.offset;
|
||||||
const Rotation originalRotation = bb.rotation;
|
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].offset == originalOffset);
|
||||||
REQUIRE(bp.buildings[0].rotation == originalRotation);
|
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]")
|
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.
|
// Two buildings left and right of center; after CW they should be above and below.
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
@@ -315,7 +351,7 @@ TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[
|
|||||||
right.offset = QPoint( 1, 0);
|
right.offset = QPoint( 1, 0);
|
||||||
bp.buildings = { left, right };
|
bp.buildings = { left, right };
|
||||||
|
|
||||||
applyRotationCW(bp);
|
applyRotationCW(bp, cfg);
|
||||||
|
|
||||||
// left (-1, 0) → CW → (0, -1) (above center)
|
// left (-1, 0) → CW → (0, -1) (above center)
|
||||||
// right ( 1, 0) → CW → (0, 1) (below 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);
|
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
|
// Simulation-level: blueprint placement places buildings at correct tiles
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <climits>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -936,7 +937,17 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
{
|
{
|
||||||
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
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);
|
bb.rotation = rotateClockwise(bb.rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -951,7 +962,17 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
{
|
{
|
||||||
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
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);
|
bb.rotation = rotateCounterClockwise(bb.rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user