#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(tile.x()) + 0.5f, static_cast(tile.y()) + 0.5f); return (center - cursorWorldPos).lengthSquared(); } } // namespace std::optional 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 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 exitPartner; if (const std::optional ahead = firstTunnelFacing(lookup, hoverTile, rotation, rotation, maxDistance); ahead.has_value()) { const std::optional 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 entryPartner; if (const std::optional behind = firstTunnelFacing(lookup, hoverTile, oppositeRotation(rotation), rotation, maxDistance); behind.has_value()) { const std::optional 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}; } std::optional findTunnelPartner(const TunnelLookup& lookup, QPoint tile, BuildingType type, Rotation rotation, int maxDistance) { if (type == BuildingType::TunnelEntry) { // An entry pairs with the first same-direction tunnel ahead, if it is an exit. const std::optional ahead = firstTunnelFacing(lookup, tile, rotation, rotation, maxDistance); if (ahead.has_value()) { const std::optional info = lookup(*ahead); if (info.has_value() && info->type == BuildingType::TunnelExit) { return ahead; } } return std::nullopt; } if (type == BuildingType::TunnelExit) { // An exit is claimed by the first same-direction tunnel behind it, if it is an // entry (its forward search reaches this exit as its first same-direction tunnel). const std::optional behind = firstTunnelFacing(lookup, tile, oppositeRotation(rotation), rotation, maxDistance); if (behind.has_value()) { const std::optional info = lookup(*behind); if (info.has_value() && info->type == BuildingType::TunnelEntry) { return behind; } } return std::nullopt; } return std::nullopt; } TunnelLookup makeTunnelLookup(const TunnelTileMap& tunnels) { return [&tunnels](QPoint tile) -> std::optional { const TunnelTileMap::const_iterator it = tunnels.find(tile); if (it == tunnels.end()) { return std::nullopt; } return it->second; }; }