Unify tunnel build mode into a single Tunnel button
This commit is contained in:
@@ -35,11 +35,21 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWid
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Tunnel Entry and Tunnel Exit share a single "Tunnel" button; the exit is
|
||||
// reached through the unified tunnel build mode, not its own button
|
||||
// (REQ-BLD-TUNNEL-MODE, REQ-UI-BUILD-GRID). Both stay player-placeable so
|
||||
// blueprints and cost totals still account for exits.
|
||||
if (def.type == BuildingType::TunnelExit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
m_types.push_back(def.type);
|
||||
m_costs[def.type] = def.cost;
|
||||
|
||||
const QString label = QString::fromStdString(toDisplayName(def.id))
|
||||
+ "\n" + tr("%1 Building Blocks").arg(def.cost);
|
||||
const QString name = (def.type == BuildingType::TunnelEntry)
|
||||
? tr("Tunnel")
|
||||
: QString::fromStdString(toDisplayName(def.id));
|
||||
const QString label = name + "\n" + tr("%1 Building Blocks").arg(def.cost);
|
||||
QPushButton* btn = new QPushButton(label, this);
|
||||
btn->setCheckable(true);
|
||||
btn->setFixedHeight(48);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -205,6 +205,16 @@ private:
|
||||
void stepSpeed(int delta);
|
||||
void placeAtTile(QPoint tile);
|
||||
|
||||
// Unified tunnel build mode (REQ-BLD-TUNNEL-MODE). Active while the builder type
|
||||
// is TunnelEntry; the ghost then resolves to an entry or exit by hovered position.
|
||||
bool inTunnelMode() const;
|
||||
// The building type the ghost currently represents: the position-resolved tunnel
|
||||
// type in tunnel mode, otherwise the plain builder type.
|
||||
BuildingType effectiveBuilderType() const;
|
||||
// Recomputes m_tunnelGhostType and m_tunnelPartnerTile from the current ghost
|
||||
// tile, rotation, and sub-tile cursor position. Only meaningful in tunnel mode.
|
||||
void updateTunnelGhost();
|
||||
|
||||
// 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 };
|
||||
@@ -274,6 +284,12 @@ private:
|
||||
Rotation m_ghostRotation;
|
||||
QPoint m_ghostTile;
|
||||
bool m_ghostValid;
|
||||
// Unified tunnel build mode (REQ-BLD-TUNNEL-MODE): while m_builderType is
|
||||
// TunnelEntry the ghost resolves to an entry or an exit based on the hovered
|
||||
// position; m_tunnelPartnerTile is the existing tunnel it would complete (drawn
|
||||
// as the green connection preview), or unset when there is no completion match.
|
||||
BuildingType m_tunnelGhostType = BuildingType::TunnelEntry;
|
||||
std::optional<QPoint> m_tunnelPartnerTile;
|
||||
// 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.
|
||||
|
||||
@@ -51,6 +51,7 @@ struct OverlayVisuals
|
||||
QColor copyConfig;
|
||||
QColor lockedAsteroid;
|
||||
QColor modalDim;
|
||||
QColor tunnelPreview; // tunnel connection preview highlight (REQ-BLD-TUNNEL-MODE)
|
||||
};
|
||||
|
||||
struct ToastVisuals
|
||||
|
||||
@@ -227,6 +227,7 @@ VisualsConfig VisualsLoader::load(const std::string& path)
|
||||
cfg.overlays.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config");
|
||||
cfg.overlays.lockedAsteroid = parseColor(requireString(ov, "locked_asteroid", "overlays"), "overlays.locked_asteroid");
|
||||
cfg.overlays.modalDim = parseColor(requireString(ov, "modal_dim", "overlays"), "overlays.modal_dim");
|
||||
cfg.overlays.tunnelPreview = parseColor(requireString(ov, "tunnel_preview", "overlays"), "overlays.tunnel_preview");
|
||||
}
|
||||
|
||||
// Toast
|
||||
|
||||
Reference in New Issue
Block a user