Unify tunnel build mode into a single Tunnel button
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
115
src/lib/core/TunnelCompletion.cpp
Normal file
115
src/lib/core/TunnelCompletion.cpp
Normal 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};
|
||||
}
|
||||
54
src/lib/core/TunnelCompletion.h
Normal file
54
src/lib/core/TunnelCompletion.h
Normal 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);
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "StateChecksum.h"
|
||||
#include "Tick.h"
|
||||
#include "TunnelCompletion.h"
|
||||
#include "tracing.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -177,55 +178,57 @@ void BeltSystem::reevaluateTunnelPairing()
|
||||
std::vector<TunnelLink> oldLinks;
|
||||
std::swap(oldLinks, m_tunnelLinks);
|
||||
|
||||
// Tunnel index over this system's own (completed) tunnel tiles, shared with the
|
||||
// scan primitive so the pairing rule lives in one place (REQ-BLD-TUNNEL-PAIR).
|
||||
const TunnelLookup lookup = [this](QPoint tile) -> std::optional<TunnelTileInfo>
|
||||
{
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
|
||||
m_tunnelEntries.find(key(tile));
|
||||
if (teIt != m_tunnelEntries.end())
|
||||
{
|
||||
return TunnelTileInfo{BuildingType::TunnelEntry, teIt->second.direction};
|
||||
}
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
|
||||
m_tunnelExits.find(key(tile));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
return TunnelTileInfo{BuildingType::TunnelExit, txIt->second.direction};
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
|
||||
{
|
||||
const QPoint entryPos(entry.first.first, entry.first.second);
|
||||
const Rotation dir = entry.second.direction;
|
||||
const int maxDist = entry.second.maxDistance;
|
||||
|
||||
for (int d = 1; d <= maxDist; ++d)
|
||||
// The first same-direction tunnel ahead forms a pair only when it is an exit;
|
||||
// a same-direction entry blocks (firstTunnelFacing stops at it either way).
|
||||
const std::optional<QPoint> target =
|
||||
firstTunnelFacing(lookup, entryPos, dir, dir, maxDist);
|
||||
if (!target.has_value() || m_tunnelExits.find(key(*target)) == m_tunnelExits.end())
|
||||
{
|
||||
QPoint probe = entryPos;
|
||||
for (int step = 0; step < d; ++step)
|
||||
{
|
||||
probe = adjacentTile(probe, dir);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if a same-direction tunnel entry is here (blocks pairing)
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
|
||||
m_tunnelEntries.find(key(probe));
|
||||
if (teIt != m_tunnelEntries.end() && teIt->second.direction == dir)
|
||||
TunnelLink link;
|
||||
link.entryTile = entryPos;
|
||||
link.exitTile = *target;
|
||||
// The exit is colinear with the entry along `dir`, so the tile-coordinate
|
||||
// distance is the Manhattan distance.
|
||||
link.length = static_cast<double>((*target - entryPos).manhattanLength());
|
||||
|
||||
for (const TunnelLink& old : oldLinks)
|
||||
{
|
||||
if (old.entryTile == entryPos && old.exitTile == *target)
|
||||
{
|
||||
link.items = old.items;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if a same-direction tunnel exit is here (forms pair)
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
|
||||
m_tunnelExits.find(key(probe));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
if (txIt->second.direction == dir)
|
||||
{
|
||||
TunnelLink link;
|
||||
link.entryTile = entryPos;
|
||||
link.exitTile = probe;
|
||||
link.length = static_cast<double>(d);
|
||||
|
||||
for (const TunnelLink& old : oldLinks)
|
||||
{
|
||||
if (old.entryTile == entryPos && old.exitTile == probe)
|
||||
{
|
||||
link.items = old.items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_tunnelLinks.push_back(std::move(link));
|
||||
break;
|
||||
}
|
||||
// Different direction exit — skip, keep searching
|
||||
}
|
||||
}
|
||||
|
||||
m_tunnelLinks.push_back(std::move(link));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user