Implement deferred L-shaped belt drag placement

This commit is contained in:
2026-07-21 21:05:31 +02:00
parent 1cbc695bc5
commit b2c1ea34fd
8 changed files with 444 additions and 31 deletions

View File

@@ -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>());