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

@@ -12,6 +12,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
PARENT_SCOPE
)
@@ -21,6 +22,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
PARENT_SCOPE
)

View File

@@ -0,0 +1,115 @@
#include "TunnelCompletion.h"
namespace
{
QPoint stepTile(QPoint tile, Rotation dir)
{
switch (dir)
{
case Rotation::North: return {tile.x(), tile.y() - 1};
case Rotation::East: return {tile.x() + 1, tile.y() };
case Rotation::South: return {tile.x(), tile.y() + 1};
case Rotation::West: return {tile.x() - 1, tile.y() };
}
return tile;
}
Rotation oppositeRotation(Rotation dir)
{
switch (dir)
{
case Rotation::North: return Rotation::South;
case Rotation::East: return Rotation::West;
case Rotation::South: return Rotation::North;
case Rotation::West: return Rotation::East;
}
return dir;
}
float tileCenterDistanceSq(QPoint tile, QVector2D cursorWorldPos)
{
const QVector2D center(static_cast<float>(tile.x()) + 0.5f,
static_cast<float>(tile.y()) + 0.5f);
return (center - cursorWorldPos).lengthSquared();
}
} // namespace
std::optional<QPoint> firstTunnelFacing(const TunnelLookup& lookup, QPoint start,
Rotation stepDir, Rotation targetFacing,
int maxDistance)
{
QPoint probe = start;
for (int distance = 1; distance <= maxDistance; ++distance)
{
probe = stepTile(probe, stepDir);
const std::optional<TunnelTileInfo> info = lookup(probe);
if (info.has_value() && info->rotation == targetFacing)
{
return probe;
}
}
return std::nullopt;
}
TunnelCompletion resolveTunnelCompletion(const TunnelLookup& lookup, QPoint hoverTile,
Rotation rotation, int maxDistance,
QVector2D cursorWorldPos)
{
// Entry-completion: a hypothetical entry at the hovered tile searches ahead (its
// facing direction) for a same-direction exit to pair with. The first
// same-direction tunnel ahead completes the entry only if it is an exit.
std::optional<QPoint> exitPartner;
if (const std::optional<QPoint> ahead =
firstTunnelFacing(lookup, hoverTile, rotation, rotation, maxDistance);
ahead.has_value())
{
const std::optional<TunnelTileInfo> info = lookup(*ahead);
if (info.has_value() && info->type == BuildingType::TunnelExit)
{
exitPartner = ahead;
}
}
// Exit-completion: a hypothetical exit at the hovered tile pairs with an existing
// entry behind it — one whose forward search (its facing direction) reaches the
// hovered tile as its first same-direction tunnel. Scanning backwards, the first
// same-direction tunnel completes the exit only if it is an entry.
std::optional<QPoint> entryPartner;
if (const std::optional<QPoint> behind =
firstTunnelFacing(lookup, hoverTile, oppositeRotation(rotation), rotation,
maxDistance);
behind.has_value())
{
const std::optional<TunnelTileInfo> info = lookup(*behind);
if (info.has_value() && info->type == BuildingType::TunnelEntry)
{
entryPartner = behind;
}
}
// Default: a TunnelEntry with no partner.
if (!entryPartner.has_value() && !exitPartner.has_value())
{
return TunnelCompletion{BuildingType::TunnelEntry, std::nullopt};
}
if (entryPartner.has_value() && !exitPartner.has_value())
{
return TunnelCompletion{BuildingType::TunnelExit, entryPartner};
}
if (exitPartner.has_value() && !entryPartner.has_value())
{
return TunnelCompletion{BuildingType::TunnelEntry, exitPartner};
}
// Both apply: complete whichever existing partner is closer to the sub-tile
// cursor position (REQ-BLD-TUNNEL-MODE).
const float entryDistanceSq = tileCenterDistanceSq(*entryPartner, cursorWorldPos);
const float exitDistanceSq = tileCenterDistanceSq(*exitPartner, cursorWorldPos);
if (entryDistanceSq <= exitDistanceSq)
{
return TunnelCompletion{BuildingType::TunnelExit, entryPartner};
}
return TunnelCompletion{BuildingType::TunnelEntry, exitPartner};
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <functional>
#include <optional>
#include <QPoint>
#include <QVector2D>
#include "BuildingType.h"
#include "Rotation.h"
// A tunnel building occupying a single tile: whether it is an entry or an exit and
// the direction it faces. Used by the tunnel pairing scan (REQ-BLD-TUNNEL-PAIR) and
// the unified tunnel build mode (REQ-BLD-TUNNEL-MODE).
struct TunnelTileInfo
{
BuildingType type; // TunnelEntry or TunnelExit
Rotation rotation; // facing direction
};
// Given a tile, returns the tunnel building on it (entry or exit) with its facing
// direction, or std::nullopt when the tile holds no tunnel building.
using TunnelLookup = std::function<std::optional<TunnelTileInfo>(QPoint)>;
// Steps from `start` in `stepDir` over the tiles at distance 1..maxDistance and
// returns the first tile whose tunnel faces `targetFacing`. Tunnel buildings facing
// any other direction are skipped, mirroring the "stop at the first same-direction
// tunnel" rule of REQ-BLD-TUNNEL-PAIR. Returns std::nullopt if none is found in range.
std::optional<QPoint> firstTunnelFacing(const TunnelLookup& lookup, QPoint start,
Rotation stepDir, Rotation targetFacing,
int maxDistance);
// Result of resolving which tunnel end the ghost should become at a hovered tile
// (REQ-BLD-TUNNEL-MODE): the type to place and the existing tunnel it would complete.
struct TunnelCompletion
{
BuildingType resolvedType; // TunnelEntry (default) or TunnelExit
std::optional<QPoint> partnerTile; // matched existing tunnel, if any
};
// Resolves the tunnel ghost type for a hover at `hoverTile` with the ghost facing
// `rotation` (REQ-BLD-TUNNEL-MODE):
// - exit-completion: placing an exit here would pair with an existing entry behind
// it (the entry's forward search reaches this tile as its first same-direction
// tunnel) — the ghost becomes a TunnelExit;
// - entry-completion: placing an entry here would pair with an existing exit ahead
// of it — the ghost stays a TunnelEntry.
// When both apply, the end whose existing partner is closer to `cursorWorldPos` (the
// sub-tile cursor position in world tile units) wins, so nudging the cursor within a
// single tile can flip the target. With no match the ghost stays a TunnelEntry with
// no partner.
TunnelCompletion resolveTunnelCompletion(const TunnelLookup& lookup, QPoint hoverTile,
Rotation rotation, int maxDistance,
QVector2D cursorWorldPos);