Implement deferred L-shaped belt drag placement
Belts are no longer placed on hover during a drag. Left-press anchors the drag; as the cursor moves, a rectilinear (L-shaped) path of belt ghosts is previewed from the anchor to the cursor — first leg parallel to the belt's current orientation, then orthogonal — with each tile auto-oriented to follow the path. Construction sites are placed on release: new valid tiles are placed (subject to cumulative affordability), tiles holding only a belt are re-oriented in place, and occupied/invalid tiles are skipped. Unaffordable tiles show no ghost. Right-click during a drag cancels it without leaving belt build mode. Path geometry is factored into a pure computeBeltDragPath() in lib/core with Catch2 coverage; GameWorldView owns the classification, affordability, and rendering against live sim state. Implements REQ-BLD-BELT-DRAG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
94
src/lib/core/BeltDragPath.cpp
Normal file
94
src/lib/core/BeltDragPath.cpp
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#include "BeltDragPath.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int signOf(int value)
|
||||||
|
{
|
||||||
|
if (value > 0) { return 1; }
|
||||||
|
if (value < 0) { return -1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction stepping from one tile to an orthogonally adjacent tile.
|
||||||
|
Rotation directionBetween(QPoint from, QPoint to)
|
||||||
|
{
|
||||||
|
const QPoint delta = to - from;
|
||||||
|
if (delta.x() > 0) { return Rotation::East; }
|
||||||
|
if (delta.x() < 0) { return Rotation::West; }
|
||||||
|
if (delta.y() > 0) { return Rotation::South; }
|
||||||
|
return Rotation::North;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BeltPathTile> computeBeltDragPath(QPoint anchor, QPoint cursor,
|
||||||
|
Rotation orientation)
|
||||||
|
{
|
||||||
|
const bool horizontalFirst =
|
||||||
|
(orientation == Rotation::East || orientation == Rotation::West);
|
||||||
|
|
||||||
|
// Build the ordered tile coordinates: first leg along the primary axis to the
|
||||||
|
// corner, then the orthogonal leg to the cursor (no duplicated corner tile).
|
||||||
|
std::vector<QPoint> coords;
|
||||||
|
if (horizontalFirst)
|
||||||
|
{
|
||||||
|
const int stepX = signOf(cursor.x() - anchor.x());
|
||||||
|
for (int x = anchor.x(); ; x += stepX)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(x, anchor.y()));
|
||||||
|
if (x == cursor.x() || stepX == 0) { break; }
|
||||||
|
}
|
||||||
|
const int stepY = signOf(cursor.y() - anchor.y());
|
||||||
|
if (stepY != 0)
|
||||||
|
{
|
||||||
|
for (int y = anchor.y() + stepY; ; y += stepY)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(cursor.x(), y));
|
||||||
|
if (y == cursor.y()) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const int stepY = signOf(cursor.y() - anchor.y());
|
||||||
|
for (int y = anchor.y(); ; y += stepY)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(anchor.x(), y));
|
||||||
|
if (y == cursor.y() || stepY == 0) { break; }
|
||||||
|
}
|
||||||
|
const int stepX = signOf(cursor.x() - anchor.x());
|
||||||
|
if (stepX != 0)
|
||||||
|
{
|
||||||
|
for (int x = anchor.x() + stepX; ; x += stepX)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(x, cursor.y()));
|
||||||
|
if (x == cursor.x()) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign each tile the direction toward the next tile; the last tile keeps its
|
||||||
|
// incoming step direction, and a single-tile path keeps the belt orientation.
|
||||||
|
std::vector<BeltPathTile> path;
|
||||||
|
path.reserve(coords.size());
|
||||||
|
const std::size_t count = coords.size();
|
||||||
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
|
{
|
||||||
|
Rotation rotation;
|
||||||
|
if (count == 1)
|
||||||
|
{
|
||||||
|
rotation = orientation;
|
||||||
|
}
|
||||||
|
else if (index + 1 < count)
|
||||||
|
{
|
||||||
|
rotation = directionBetween(coords[index], coords[index + 1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rotation = directionBetween(coords[index - 1], coords[index]);
|
||||||
|
}
|
||||||
|
path.push_back(BeltPathTile{ coords[index], rotation });
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
27
src/lib/core/BeltDragPath.h
Normal file
27
src/lib/core/BeltDragPath.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// One tile of a belt drag-placement path: the tile coordinate and the belt
|
||||||
|
// orientation it should be given (REQ-BLD-BELT-DRAG).
|
||||||
|
struct BeltPathTile
|
||||||
|
{
|
||||||
|
QPoint tile;
|
||||||
|
Rotation rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computes the rectilinear (L-shaped) belt path from `anchor` to `cursor` for a
|
||||||
|
// belt whose current orientation is `orientation` (REQ-BLD-BELT-DRAG). The path
|
||||||
|
// first runs along the axis parallel to `orientation` (horizontal for East/West,
|
||||||
|
// vertical for North/South), stepping toward the cursor's coordinate on that axis
|
||||||
|
// to the corner tile, then runs along the orthogonal axis to the cursor tile. Each
|
||||||
|
// tile is oriented to point toward the next tile along the path; the final tile
|
||||||
|
// keeps the direction of its incoming step, and a single-tile path keeps
|
||||||
|
// `orientation`. Returned tiles are ordered from anchor to cursor with no duplicate
|
||||||
|
// corner tile.
|
||||||
|
std::vector<BeltPathTile> computeBeltDragPath(QPoint anchor, QPoint cursor,
|
||||||
|
Rotation orientation);
|
||||||
@@ -11,6 +11,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
157
src/test/BeltDragPathTest.cpp
Normal file
157
src/test/BeltDragPathTest.cpp
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BeltDragPath.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static std::vector<QPoint> tilesOf(const std::vector<BeltPathTile>& path)
|
||||||
|
{
|
||||||
|
std::vector<QPoint> tiles;
|
||||||
|
for (const BeltPathTile& entry : path) { tiles.push_back(entry.tile); }
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<Rotation> rotationsOf(const std::vector<BeltPathTile>& path)
|
||||||
|
{
|
||||||
|
std::vector<Rotation> rotations;
|
||||||
|
for (const BeltPathTile& entry : path) { rotations.push_back(entry.rotation); }
|
||||||
|
return rotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Single tile
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Single-tile path keeps the belt orientation")
|
||||||
|
{
|
||||||
|
for (Rotation orientation : { Rotation::North, Rotation::East,
|
||||||
|
Rotation::South, Rotation::West })
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(3, 4), QPoint(3, 4), orientation);
|
||||||
|
REQUIRE(path.size() == 1);
|
||||||
|
REQUIRE(path[0].tile == QPoint(3, 4));
|
||||||
|
REQUIRE(path[0].rotation == orientation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Straight runs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Straight horizontal run faces along the row toward the cursor")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(3, 0), Rotation::East);
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(1, 0), QPoint(2, 0), QPoint(3, 0) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::East, Rotation::East, Rotation::East, Rotation::East });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Straight horizontal run toward the left faces West")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(-2, 0), Rotation::East);
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(-1, 0), QPoint(-2, 0) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::West, Rotation::West, Rotation::West });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Straight vertical run faces along the column toward the cursor")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(0, 3), Rotation::South);
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(0, 1), QPoint(0, 2), QPoint(0, 3) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::South, Rotation::South, Rotation::South, Rotation::South });
|
||||||
|
}
|
||||||
|
|
||||||
|
// A vertical target with a horizontal orientation still yields a straight vertical
|
||||||
|
// line (the parallel-axis leg is zero-length).
|
||||||
|
TEST_CASE("Vertical target with horizontal orientation is a straight vertical line")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(2, 0), QPoint(2, 2), Rotation::East);
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(2, 0), QPoint(2, 1), QPoint(2, 2) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::South, Rotation::South, Rotation::South });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// L-shaped paths — horizontal-first (East/West orientation)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("East orientation goes horizontal then vertical (down-right)")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(2, 2), Rotation::East);
|
||||||
|
// Leg 1 East to the corner (2,0), then Leg 2 South to the cursor (2,2).
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(1, 0), QPoint(2, 0), QPoint(2, 1), QPoint(2, 2) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::East, Rotation::East, Rotation::South, Rotation::South,
|
||||||
|
Rotation::South });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("East orientation with cursor up-left goes horizontal (West) then vertical (North)")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(-2, -2), Rotation::East);
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(-1, 0), QPoint(-2, 0), QPoint(-2, -1),
|
||||||
|
QPoint(-2, -2) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::West, Rotation::West, Rotation::North, Rotation::North,
|
||||||
|
Rotation::North });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("West orientation is horizontal-first as well (up-right cursor)")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(2, -2), Rotation::West);
|
||||||
|
// Horizontal axis first: East toward the cursor to the corner (2,0), then North.
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(1, 0), QPoint(2, 0), QPoint(2, -1), QPoint(2, -2) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::East, Rotation::East, Rotation::North, Rotation::North,
|
||||||
|
Rotation::North });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// L-shaped paths — vertical-first (North/South orientation)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("South orientation goes vertical then horizontal (down-right)")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(2, 2), Rotation::South);
|
||||||
|
// Leg 1 South to the corner (0,2), then Leg 2 East to the cursor (2,2).
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(0, 1), QPoint(0, 2), QPoint(1, 2), QPoint(2, 2) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::South, Rotation::South, Rotation::East, Rotation::East,
|
||||||
|
Rotation::East });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("North orientation is vertical-first (down-left cursor)")
|
||||||
|
{
|
||||||
|
const std::vector<BeltPathTile> path =
|
||||||
|
computeBeltDragPath(QPoint(0, 0), QPoint(-2, 2), Rotation::North);
|
||||||
|
// Vertical axis first: South toward the cursor to the corner (0,2), then West.
|
||||||
|
REQUIRE(tilesOf(path) == std::vector<QPoint>{
|
||||||
|
QPoint(0, 0), QPoint(0, 1), QPoint(0, 2), QPoint(-1, 2), QPoint(-2, 2) });
|
||||||
|
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
|
||||||
|
Rotation::South, Rotation::South, Rotation::West, Rotation::West,
|
||||||
|
Rotation::West });
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ add_files(
|
|||||||
SimulationTest.cpp
|
SimulationTest.cpp
|
||||||
BeltSystemTest.cpp
|
BeltSystemTest.cpp
|
||||||
SurfaceMaskTest.cpp
|
SurfaceMaskTest.cpp
|
||||||
|
BeltDragPathTest.cpp
|
||||||
BuildingTest.cpp
|
BuildingTest.cpp
|
||||||
BuildingConfigTest.cpp
|
BuildingConfigTest.cpp
|
||||||
ShipTest.cpp
|
ShipTest.cpp
|
||||||
|
|||||||
@@ -903,23 +903,12 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For placements whose UI follow-up depends on success (belt-drag bookkeeping,
|
// For placements whose UI follow-up depends on success (the tunnel entry/exit
|
||||||
// tunnel entry/exit toggle), pre-validate occupancy + affordability so the
|
// toggle), pre-validate occupancy + affordability so the optimistic UI update
|
||||||
// optimistic UI update matches what the deferred command will do — isValidPlacement
|
// matches what the deferred command will do — isValidPlacement (above) already
|
||||||
// (above) already covered terrain/bounds.
|
// covered terrain/bounds. Belts are placed via the drag path (applyBeltDragPath),
|
||||||
if (type == BuildingType::Belt)
|
// not here.
|
||||||
{
|
if (type == BuildingType::Splitter
|
||||||
if (m_beltDragTiles.count(tile) > 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
|
|
||||||
{
|
|
||||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
|
||||||
m_beltDragTiles.insert(tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (type == BuildingType::Splitter
|
|
||||||
|| type == BuildingType::TunnelEntry
|
|| type == BuildingType::TunnelEntry
|
||||||
|| type == BuildingType::TunnelExit)
|
|| type == BuildingType::TunnelExit)
|
||||||
{
|
{
|
||||||
@@ -942,6 +931,80 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Belt drag placement (REQ-BLD-BELT-DRAG)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
||||||
|
{
|
||||||
|
m_beltDragPath = computeBeltDragPath(m_beltDragAnchor, cursorTile, m_ghostRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
||||||
|
{
|
||||||
|
std::vector<BeltDragResolved> resolved;
|
||||||
|
resolved.reserve(m_beltDragPath.size());
|
||||||
|
|
||||||
|
const BuildingDef* def = findBuildingDef(BuildingType::Belt);
|
||||||
|
const int beltCost = (def != nullptr) ? def->cost : 0;
|
||||||
|
const int stock = m_sim->getBuildingBlocksStock();
|
||||||
|
int spent = 0;
|
||||||
|
|
||||||
|
for (const BeltPathTile& entry : m_beltDragPath)
|
||||||
|
{
|
||||||
|
BeltDragResolved item;
|
||||||
|
const std::optional<BuildingId> rotateTarget =
|
||||||
|
m_sim->getBuildings().findRotateInPlaceTarget(
|
||||||
|
BuildingType::Belt, entry.tile, entry.rotation);
|
||||||
|
if (rotateTarget.has_value())
|
||||||
|
{
|
||||||
|
// A tile holding only a belt (or belt site) is re-oriented, no cost.
|
||||||
|
item.action = BeltTileAction::RotateInPlace;
|
||||||
|
item.affordable = true;
|
||||||
|
item.rotateId = rotateTarget;
|
||||||
|
}
|
||||||
|
else if (isValidPlacement(BuildingType::Belt, entry.tile, entry.rotation))
|
||||||
|
{
|
||||||
|
// Empty, valid cell: a new belt, subject to cumulative affordability.
|
||||||
|
item.action = BeltTileAction::PlaceNew;
|
||||||
|
item.affordable = (spent + beltCost <= stock);
|
||||||
|
item.rotateId = std::nullopt;
|
||||||
|
if (item.affordable) { spent += beltCost; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Occupied by a non-belt building/site, or otherwise invalid terrain.
|
||||||
|
item.action = BeltTileAction::Invalid;
|
||||||
|
item.affordable = false;
|
||||||
|
item.rotateId = std::nullopt;
|
||||||
|
}
|
||||||
|
resolved.push_back(item);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::applyBeltDragPath()
|
||||||
|
{
|
||||||
|
const std::vector<BeltDragResolved> resolved = resolveBeltDragPath();
|
||||||
|
for (std::size_t index = 0; index < resolved.size(); ++index)
|
||||||
|
{
|
||||||
|
const BeltDragResolved& item = resolved[index];
|
||||||
|
const BeltPathTile& entry = m_beltDragPath[index];
|
||||||
|
if (item.action == BeltTileAction::PlaceNew && item.affordable)
|
||||||
|
{
|
||||||
|
enqueuePlaceBuilding(BuildingType::Belt, entry.tile, entry.rotation);
|
||||||
|
}
|
||||||
|
else if (item.action == BeltTileAction::RotateInPlace)
|
||||||
|
{
|
||||||
|
std::shared_ptr<RotateInPlaceCommand> command =
|
||||||
|
std::make_shared<RotateInPlaceCommand>();
|
||||||
|
command->id = *item.rotateId;
|
||||||
|
command->newRotation = entry.rotation;
|
||||||
|
enqueueCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Port glyph helper
|
// Port glyph helper
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1658,11 +1721,34 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
{
|
{
|
||||||
// Builder-mode ghost
|
// Builder-mode ghost
|
||||||
if (m_builderType.has_value())
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
if (*m_builderType == BuildingType::Belt && m_dragging)
|
||||||
|
{
|
||||||
|
// Belt drag: a ghost per path tile (REQ-BLD-BELT-DRAG). Rotate-in-place
|
||||||
|
// and affordable new tiles use the belt colors; occupied/invalid tiles
|
||||||
|
// use the invalid color; unaffordable tiles show no ghost at all.
|
||||||
|
const std::vector<BeltDragResolved> resolved = resolveBeltDragPath();
|
||||||
|
for (std::size_t index = 0; index < resolved.size(); ++index)
|
||||||
|
{
|
||||||
|
const BeltDragResolved& item = resolved[index];
|
||||||
|
if (item.action == BeltTileAction::PlaceNew && !item.affordable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const BeltPathTile& entry = m_beltDragPath[index];
|
||||||
|
drawBuildingGhost(painter, BuildingType::Belt, entry.tile,
|
||||||
|
entry.rotation,
|
||||||
|
/*valid*/ item.action != BeltTileAction::Invalid,
|
||||||
|
/*showPortTargetGlyphs*/ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
|
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
|
||||||
m_ghostRotation, m_ghostValid,
|
m_ghostRotation, m_ghostValid,
|
||||||
/*showPortTargetGlyphs*/ true);
|
/*showPortTargetGlyphs*/ true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Blueprint placement ghost
|
// Blueprint placement ghost
|
||||||
if (m_blueprintMode.has_value())
|
if (m_blueprintMode.has_value())
|
||||||
@@ -2061,7 +2147,20 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
if (event->button() == Qt::RightButton)
|
if (event->button() == Qt::RightButton)
|
||||||
{
|
{
|
||||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
if (m_dragging)
|
||||||
|
{
|
||||||
|
// Cancel the in-progress belt drag without placing anything;
|
||||||
|
// stay in belt builder mode (REQ-BLD-BELT-DRAG).
|
||||||
|
m_dragging = false;
|
||||||
|
m_beltDragPath.clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exitBuilderMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
||||||
else if (m_demolishMode) { toggleDemolishMode(); }
|
else if (m_demolishMode) { toggleDemolishMode(); }
|
||||||
else if (event->modifiers() & Qt::ShiftModifier)
|
else if (event->modifiers() & Qt::ShiftModifier)
|
||||||
@@ -2084,9 +2183,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
const BuildingType type = *m_builderType;
|
const BuildingType type = *m_builderType;
|
||||||
if (type == BuildingType::Belt)
|
if (type == BuildingType::Belt)
|
||||||
{
|
{
|
||||||
|
// Deferred placement: start the drag and show the path ghost; nothing
|
||||||
|
// is placed until release (REQ-BLD-BELT-DRAG).
|
||||||
m_dragging = true;
|
m_dragging = true;
|
||||||
m_beltDragTiles.clear();
|
m_beltDragAnchor = tile;
|
||||||
placeAtTile(tile);
|
recomputeBeltDragPath(tile);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2257,7 +2358,9 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
if (m_dragging)
|
if (m_dragging)
|
||||||
{
|
{
|
||||||
placeAtTile(tile);
|
// Belt drag: update the previewed path; placement happens on release
|
||||||
|
// (REQ-BLD-BELT-DRAG).
|
||||||
|
recomputeBeltDragPath(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (m_blueprintMode.has_value())
|
else if (m_blueprintMode.has_value())
|
||||||
@@ -2281,8 +2384,11 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
if (m_dragging)
|
if (m_dragging)
|
||||||
{
|
{
|
||||||
|
// Apply the previewed belt path now that the button is released
|
||||||
|
// (REQ-BLD-BELT-DRAG).
|
||||||
|
applyBeltDragPath();
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
m_beltDragTiles.clear();
|
m_beltDragPath.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_boxSelecting)
|
if (m_boxSelecting)
|
||||||
@@ -2565,7 +2671,7 @@ void GameWorldView::exitBlueprintMode()
|
|||||||
void GameWorldView::exitBuilderMode()
|
void GameWorldView::exitBuilderMode()
|
||||||
{
|
{
|
||||||
m_builderType.reset();
|
m_builderType.reset();
|
||||||
m_beltDragTiles.clear();
|
m_beltDragPath.clear();
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<BuilderModeExitedEvent>());
|
std::make_shared<BuilderModeExitedEvent>());
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "SpeedChangeRequestedEvent.h"
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
|
#include "BeltDragPath.h"
|
||||||
#include "CommandManager.h"
|
#include "CommandManager.h"
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -204,6 +205,24 @@ private:
|
|||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
|
// Belt drag placement (REQ-BLD-BELT-DRAG).
|
||||||
|
// Per-path-tile decision, shared by ghost drawing and release-time placement.
|
||||||
|
enum class BeltTileAction { PlaceNew, RotateInPlace, Invalid };
|
||||||
|
struct BeltDragResolved
|
||||||
|
{
|
||||||
|
BeltTileAction action;
|
||||||
|
bool affordable; // meaningful only for PlaceNew
|
||||||
|
std::optional<BuildingId> rotateId; // set only for RotateInPlace
|
||||||
|
};
|
||||||
|
// Recomputes m_beltDragPath from m_beltDragAnchor to cursorTile using the
|
||||||
|
// current ghost orientation.
|
||||||
|
void recomputeBeltDragPath(QPoint cursorTile);
|
||||||
|
// Classifies each path tile against the current sim state, applying cumulative
|
||||||
|
// affordability to the PlaceNew tiles.
|
||||||
|
std::vector<BeltDragResolved> resolveBeltDragPath() const;
|
||||||
|
// Enqueues placements and rotate-in-place commands for the resolved path.
|
||||||
|
void applyBeltDragPath();
|
||||||
|
|
||||||
// Copy-settings gesture (REQ-BLD-COPY-CONFIG): Shift+right-click copies a
|
// Copy-settings gesture (REQ-BLD-COPY-CONFIG): Shift+right-click copies a
|
||||||
// building's configuration into m_copiedConfig; Shift+left-click applies it to
|
// building's configuration into m_copiedConfig; Shift+left-click applies it to
|
||||||
// another building of the same type via the existing configuration commands.
|
// another building of the same type via the existing configuration commands.
|
||||||
@@ -255,7 +274,11 @@ private:
|
|||||||
Rotation m_ghostRotation;
|
Rotation m_ghostRotation;
|
||||||
QPoint m_ghostTile;
|
QPoint m_ghostTile;
|
||||||
bool m_ghostValid;
|
bool m_ghostValid;
|
||||||
std::set<QPoint, QPointCompare> m_beltDragTiles;
|
// Deferred belt drag placement (REQ-BLD-BELT-DRAG): while dragging, the
|
||||||
|
// rectilinear anchor->cursor path is recomputed on each move and only applied
|
||||||
|
// on release. Empty unless a belt drag is in progress.
|
||||||
|
std::vector<BeltPathTile> m_beltDragPath;
|
||||||
|
QPoint m_beltDragAnchor;
|
||||||
bool m_dragging;
|
bool m_dragging;
|
||||||
|
|
||||||
std::optional<Blueprint> m_blueprintMode;
|
std::optional<Blueprint> m_blueprintMode;
|
||||||
|
|||||||
Reference in New Issue
Block a user