Implement deferred L-shaped belt drag placement
This commit is contained in:
@@ -100,13 +100,16 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
- REQ-BLD-COST: The player places buildings from a build menu. Placement costs building blocks from the global stock. The cost per building type is read from `buildings.toml [[building]].cost`.
|
||||
- REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct.
|
||||
- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space).
|
||||
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again.
|
||||
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again. (Exception: while a belt drag placement is in progress, right-clicking cancels that drag instead of exiting, and builder mode stays active — REQ-BLD-BELT-DRAG.)
|
||||
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. The ghost is drawn semi-transparently in the building type's own visuals — its `fill` and `outline` colors and `glyph` from `visuals.toml` — so that different building types are visually distinguishable in builder mode rather than all looking alike. When the current cursor position is invalid, the ghost instead uses the distinct "invalid" color (REQ-BLD-PLACE-VALID), which overrides the per-building coloring.
|
||||
- REQ-BLD-ROTATE: While in builder mode, pressing Shift+R rotates the ghost 90° clockwise and R rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
|
||||
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock.
|
||||
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock. (For belts, placement is instead deferred to a drag gesture and happens on mouse release — REQ-BLD-BELT-DRAG.)
|
||||
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color — overriding its per-building coloring (REQ-BLD-GHOST) — when the current cursor position fails any of these conditions.
|
||||
- REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost).
|
||||
- REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture.
|
||||
- REQ-BLD-BELT-DRAG: **Belt drag placement.** For belts, placement is a deferred drag gesture rather than immediate per-tile placement: construction sites are not placed while the cursor hovers new tiles, but only once the player releases the left mouse button. Pressing the left mouse button in the game world while in belt builder mode starts a drag anchored at the tile under the cursor. As the cursor moves, a **rectilinear (L-shaped) path** of belt tiles is computed from the anchor tile to the tile under the cursor: the path first runs along the axis **parallel to the belt's current orientation** (REQ-BLD-ROTATE) — stepping toward the cursor's coordinate on that axis to a corner tile — and then runs along the orthogonal axis to the cursor tile. When the cursor shares the anchor's row or column the path degenerates to a straight line, and when it is on the anchor tile the path is a single tile.
|
||||
- **Ghosts.** While dragging, a belt ghost (REQ-BLD-GHOST) is rendered on every path tile that would be acted on, instead of a single ghost under the cursor. Each ghost is oriented to point toward the next tile along the path toward the cursor, so the path forms one connected belt run that turns at the corner (curved belts along the path auto-derive per REQ-BLD-BELT); the final tile keeps the direction of its incoming step, and a single-tile path keeps the belt's current orientation. A tile occupied by only an existing belt or belt construction site is a valid target — its belt is re-oriented to follow the path — and shows a normal belt ghost. A tile occupied by a non-belt building or construction site, or otherwise an invalid belt position (REQ-BLD-PLACE-VALID), is drawn in the distinct invalid color, overriding the belt coloring. A tile whose new belt is unaffordable — the cumulative cost of the belts newly placed up to and including it exceeds the global stock — shows **no ghost at all**.
|
||||
- **Placement on release.** No construction site is placed while dragging. On releasing the left mouse button, the path is applied in order (anchor to cursor): each cell occupied by only an existing belt or belt construction site has that belt re-oriented in place to its path direction, consuming no building blocks and preserving any construction progress (REQ-BLD-ROTATE-IN-PLACE); each empty, valid cell gets a new belt construction site, consuming building blocks from the global stock (REQ-BLD-COST). Cells occupied by a non-belt building or construction site, cells that are otherwise invalid (REQ-BLD-PLACE-VALID), and cells whose new belt can no longer be afforded once the running total has been spent are skipped. This supersedes the click-to-place of REQ-BLD-PLACE for belts, including both the single-tile case and multi-tile drags that pass over existing belts.
|
||||
- **Right-click cancels the drag.** Right-clicking while a belt drag is in progress cancels it: the path is discarded, no construction site is placed, and belt builder mode stays active (the exception to REQ-BLD-BUILDER-MODE). Right-clicking when no drag is in progress exits builder mode as usual (REQ-BLD-BUILDER-MODE).
|
||||
- REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type.
|
||||
- REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is removed from the queue and the **full** building block cost is refunded. The HQ and player defence stations cannot be demolished.
|
||||
- REQ-BLD-DEMOLISH-CLICK: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), left-clicking a placed factory building or construction site in the game world demolishes it, following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Clicking a building that cannot be demolished (the HQ or a player defence station, per REQ-BLD-DEMOLISH), or clicking empty world space, has no effect. Demolish mode stays active after a demolition so the player can demolish further buildings without re-entering the mode; it is exited via the Q toggle (REQ-UI-HOTKEYS) or the Demolish button (REQ-UI-DEMOLISH-BUTTON).
|
||||
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1658,11 +1721,34 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
||||
{
|
||||
// Builder-mode ghost
|
||||
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,
|
||||
m_ghostRotation, m_ghostValid,
|
||||
/*showPortTargetGlyphs*/ true);
|
||||
}
|
||||
}
|
||||
|
||||
// Blueprint placement ghost
|
||||
if (m_blueprintMode.has_value())
|
||||
@@ -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)
|
||||
{
|
||||
// Deferred placement: start the drag and show the path ghost; nothing
|
||||
// is placed until release (REQ-BLD-BELT-DRAG).
|
||||
m_dragging = true;
|
||||
m_beltDragTiles.clear();
|
||||
placeAtTile(tile);
|
||||
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