Implement deferred L-shaped belt drag placement
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}/SchematicChoiceOption.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -19,6 +20,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||
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
|
||||
BeltSystemTest.cpp
|
||||
SurfaceMaskTest.cpp
|
||||
BeltDragPathTest.cpp
|
||||
BuildingTest.cpp
|
||||
BuildingConfigTest.cpp
|
||||
ShipTest.cpp
|
||||
|
||||
@@ -903,23 +903,12 @@ void GameWorldView::placeAtTile(QPoint tile)
|
||||
return;
|
||||
}
|
||||
|
||||
// For placements whose UI follow-up depends on success (belt-drag bookkeeping,
|
||||
// tunnel entry/exit toggle), pre-validate occupancy + affordability so the
|
||||
// optimistic UI update matches what the deferred command will do — isValidPlacement
|
||||
// (above) already covered terrain/bounds.
|
||||
if (type == BuildingType::Belt)
|
||||
{
|
||||
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
|
||||
// For placements whose UI follow-up depends on success (the tunnel entry/exit
|
||||
// toggle), pre-validate occupancy + affordability so the optimistic UI update
|
||||
// matches what the deferred command will do — isValidPlacement (above) already
|
||||
// covered terrain/bounds. Belts are placed via the drag path (applyBeltDragPath),
|
||||
// not here.
|
||||
if (type == BuildingType::Splitter
|
||||
|| type == BuildingType::TunnelEntry
|
||||
|| 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1659,9 +1722,32 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
||||
// Builder-mode ghost
|
||||
if (m_builderType.has_value())
|
||||
{
|
||||
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
|
||||
m_ghostRotation, m_ghostValid,
|
||||
/*showPortTargetGlyphs*/ true);
|
||||
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,
|
||||
m_ghostRotation, m_ghostValid,
|
||||
/*showPortTargetGlyphs*/ true);
|
||||
}
|
||||
}
|
||||
|
||||
// Blueprint placement ghost
|
||||
@@ -2061,7 +2147,20 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
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_demolishMode) { toggleDemolishMode(); }
|
||||
else if (event->modifiers() & Qt::ShiftModifier)
|
||||
@@ -2084,9 +2183,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
const BuildingType type = *m_builderType;
|
||||
if (type == BuildingType::Belt)
|
||||
{
|
||||
m_dragging = true;
|
||||
m_beltDragTiles.clear();
|
||||
placeAtTile(tile);
|
||||
// Deferred placement: start the drag and show the path ghost; nothing
|
||||
// is placed until release (REQ-BLD-BELT-DRAG).
|
||||
m_dragging = true;
|
||||
m_beltDragAnchor = tile;
|
||||
recomputeBeltDragPath(tile);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2257,7 +2358,9 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||
|
||||
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())
|
||||
@@ -2281,8 +2384,11 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
|
||||
if (m_dragging)
|
||||
{
|
||||
// Apply the previewed belt path now that the button is released
|
||||
// (REQ-BLD-BELT-DRAG).
|
||||
applyBeltDragPath();
|
||||
m_dragging = false;
|
||||
m_beltDragTiles.clear();
|
||||
m_beltDragPath.clear();
|
||||
}
|
||||
|
||||
if (m_boxSelecting)
|
||||
@@ -2565,7 +2671,7 @@ void GameWorldView::exitBlueprintMode()
|
||||
void GameWorldView::exitBuilderMode()
|
||||
{
|
||||
m_builderType.reset();
|
||||
m_beltDragTiles.clear();
|
||||
m_beltDragPath.clear();
|
||||
m_dragging = false;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BuilderModeExitedEvent>());
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "SpeedChangeRequestedEvent.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "BeltDragPath.h"
|
||||
#include "CommandManager.h"
|
||||
#include "EntitySelectionChangedEvent.h"
|
||||
#include "GameConfig.h"
|
||||
@@ -204,6 +205,24 @@ private:
|
||||
void stepSpeed(int delta);
|
||||
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
|
||||
// building's configuration into m_copiedConfig; Shift+left-click applies it to
|
||||
// another building of the same type via the existing configuration commands.
|
||||
@@ -255,7 +274,11 @@ private:
|
||||
Rotation m_ghostRotation;
|
||||
QPoint m_ghostTile;
|
||||
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;
|
||||
|
||||
std::optional<Blueprint> m_blueprintMode;
|
||||
|
||||
Reference in New Issue
Block a user