Unify tunnel build mode into a single Tunnel button

This commit is contained in:
2026-07-21 21:12:06 +02:00
parent 4ca5b332cd
commit 9b63af6ccb
14 changed files with 522 additions and 63 deletions

View File

@@ -58,6 +58,7 @@
#include "ScrapDataComponent.h"
#include "SurfaceMask.h"
#include "Tick.h"
#include "TunnelCompletion.h"
#include "EscapeMenuRequestedEvent.h"
#include "TracePrintRequestedEvent.h"
#include "BuildHotkeyPressedEvent.h"
@@ -878,13 +879,68 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
}
}
bool GameWorldView::inTunnelMode() const
{
return m_builderType.has_value() && *m_builderType == BuildingType::TunnelEntry;
}
BuildingType GameWorldView::effectiveBuilderType() const
{
return inTunnelMode() ? m_tunnelGhostType : *m_builderType;
}
void GameWorldView::updateTunnelGhost()
{
m_tunnelGhostType = BuildingType::TunnelEntry;
m_tunnelPartnerTile.reset();
// The connection preview and entry/exit switch only apply at a valid placement
// (REQ-BLD-TUNNEL-MODE); at an invalid position the ghost stays a plain entry.
if (!m_ghostValid)
{
return;
}
// Index every tunnel entry/exit — built or still a construction site — by its
// single-cell tile, so a just-placed entry (not yet constructed) is matchable.
std::map<std::pair<int, int>, TunnelTileInfo> tunnels;
for (const Building& b : m_sim->getBuildings().getAllBuildings())
{
if (b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
{
tunnels[{b.anchor.x(), b.anchor.y()}] = TunnelTileInfo{b.type, b.rotation};
}
}
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{
if (s.type == BuildingType::TunnelEntry || s.type == BuildingType::TunnelExit)
{
tunnels[{s.anchor.x(), s.anchor.y()}] = TunnelTileInfo{s.type, s.rotation};
}
}
const TunnelLookup lookup = [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
{
const std::map<std::pair<int, int>, TunnelTileInfo>::const_iterator it =
tunnels.find({tile.x(), tile.y()});
if (it == tunnels.end()) { return std::nullopt; }
return it->second;
};
const TunnelCompletion completion =
resolveTunnelCompletion(lookup, m_ghostTile, m_ghostRotation,
m_config->world.tunnelMaxDistance_tiles, m_cursorWorldPos);
m_tunnelGhostType = completion.resolvedType;
m_tunnelPartnerTile = completion.partnerTile;
}
void GameWorldView::placeAtTile(QPoint tile)
{
if (!m_builderType.has_value())
{
return;
}
const BuildingType type = *m_builderType;
const BuildingType type = effectiveBuilderType();
if (!isValidPlacement(type, tile, m_ghostRotation))
{
@@ -903,11 +959,11 @@ void GameWorldView::placeAtTile(QPoint tile)
return;
}
// 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.
// For the splitter and tunnels, pre-validate occupancy + affordability so the
// optimistic UI update matches what the deferred command will do — isValidPlacement
// (above) already covered terrain/bounds. In tunnel mode the resolved type (entry
// or exit) is placed as-is (REQ-BLD-TUNNEL-MODE); there is no post-placement toggle.
// Belts are placed via the drag path (applyBeltDragPath), not here.
if (type == BuildingType::Splitter
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit)
@@ -915,14 +971,6 @@ void GameWorldView::placeAtTile(QPoint tile)
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
{
enqueuePlaceBuilding(type, tile, m_ghostRotation);
if (type == BuildingType::TunnelEntry)
{
m_builderType = BuildingType::TunnelExit;
}
else if (type == BuildingType::TunnelExit)
{
m_builderType = BuildingType::TunnelEntry;
}
}
}
else
@@ -1798,7 +1846,27 @@ void GameWorldView::drawOverlays(QPainter& painter)
}
else
{
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
// In tunnel mode the ghost shows the position-resolved type (entry or
// exit) and, when it would complete an existing tunnel, the matched end
// and the tiles between it and the ghost are tinted green
// (REQ-BLD-TUNNEL-MODE).
if (inTunnelMode() && m_ghostValid && m_tunnelPartnerTile.has_value())
{
const QColor green = m_visuals->overlays.tunnelPreview;
painter.fillRect(tileRect(*m_tunnelPartnerTile), green);
// Partner and ghost tile are colinear along the tunnel run; tint the
// tiles strictly between them.
const QPoint delta = *m_tunnelPartnerTile - m_ghostTile;
const QPoint step((delta.x() > 0) - (delta.x() < 0),
(delta.y() > 0) - (delta.y() < 0));
for (QPoint t = m_ghostTile + step; t != *m_tunnelPartnerTile; t += step)
{
painter.fillRect(tileRect(t), green);
}
}
drawBuildingGhost(painter, effectiveBuilderType(), m_ghostTile,
m_ghostRotation, m_ghostValid,
/*showPortTargetGlyphs*/ true);
}
@@ -2099,8 +2167,7 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
{
case 1: type = BuildingType::Belt; break;
case 2: type = BuildingType::Splitter; break;
case 3: type = BuildingType::TunnelEntry; break;
case 4: type = BuildingType::TunnelExit; break;
case 3: type = BuildingType::TunnelEntry; break; // unified tunnel mode (REQ-BLD-TUNNEL-MODE); 4 unused
}
}
else
@@ -2412,6 +2479,13 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
m_ghostTile = tile;
m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation);
if (inTunnelMode())
{
// Resolve entry vs exit and the completion partner for the new hover
// position and sub-tile cursor (REQ-BLD-TUNNEL-MODE).
updateTunnelGhost();
}
if (m_dragging)
{
// Belt drag: update the previewed path; placement happens on release
@@ -2587,6 +2661,8 @@ void GameWorldView::rotateGhost(bool clockwise)
m_ghostRotation = clockwise ? rotateClockwise(m_ghostRotation)
: rotateCounterClockwise(m_ghostRotation);
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
// A new facing changes which tunnels the ghost could complete (REQ-BLD-TUNNEL-MODE).
if (inTunnelMode()) { updateTunnelGhost(); }
// Rotating during a belt drag re-picks the path's primary axis immediately,
// without waiting for the next mouse move (REQ-BLD-BELT-DRAG).
if (m_dragging) { recomputeBeltDragPath(m_ghostTile); }
@@ -2701,9 +2777,11 @@ void GameWorldView::pasteConfigTo(BuildingId id)
void GameWorldView::enterBuilderMode(BuildingType type)
{
m_builderType = type;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_builderType = type;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_tunnelGhostType = BuildingType::TunnelEntry;
m_tunnelPartnerTile.reset();
m_demolishMode = false;
m_blueprintMode.reset();
EventManager::getInstance()->sendEventImmediately(