Implement deferred L-shaped belt drag placement
This commit is contained in:
@@ -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