Implement unified tunnel build mode (REQ-BLD-TUNNEL-MODE)
Collapse the two tunnel buttons into a single Tunnel button whose ghost resolves between Tunnel Entry and Tunnel Exit based on the hovered position, with a green connection preview. - Add pure core helper TunnelCompletion (firstTunnelFacing + resolveTunnelCompletion): the entry/exit completion scans and the sub-tile cursor tie-break, unit-tested in TunnelCompletionTest. - Dedup BeltSystem::reevaluateTunnelPairing onto firstTunnelFacing so the pairing scan rule lives in one place. - GameWorldView: resolve the ghost type on hover/rotate (matching built tunnels and construction sites), place the resolved type (drop the old auto-switch), and draw the matched partner + between-tiles in green. Hotkey 3 = Tunnel, 4 unused. - BuildButtonGrid: one "Tunnel" button; tunnel_exit stays player-placeable so blueprints and cost totals still account for exits. - Add the green tunnel_preview overlay color (VisualsConfig/Loader/toml). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -16,7 +16,7 @@ surface_mask = ["<A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "tunnel_entry"
|
id = "tunnel_entry"
|
||||||
tooltip = "Sends items underground to a matching tunnel exit, letting belts cross."
|
tooltip = "Sends items underground so belts can cross. Places an entry, or an exit when it would connect to a matching entry under the cursor."
|
||||||
cost = 5
|
cost = 5
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ selected_outline = "#ffff00" # outline drawn around currently-selected buildin
|
|||||||
copy_config = "#33ccff66" # copy-settings eligible-target tint + copy/paste flash (REQ-BLD-COPY-CONFIG-FEEDBACK)
|
copy_config = "#33ccff66" # copy-settings eligible-target tint + copy/paste flash (REQ-BLD-COPY-CONFIG-FEEDBACK)
|
||||||
locked_asteroid = "#0000007f" # tint over the asteroid left of the buildable edge (not yet unlocked by expansion)
|
locked_asteroid = "#0000007f" # tint over the asteroid left of the buildable edge (not yet unlocked by expansion)
|
||||||
modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs/menus (REQ-UI-MODAL-DIM)
|
modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs/menus (REQ-UI-MODAL-DIM)
|
||||||
|
tunnel_preview = "#00ff0055" # tunnel connection preview: matched end + tiles between (REQ-BLD-TUNNEL-MODE)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||||
PARENT_SCOPE
|
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 "StateChecksum.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TunnelCompletion.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -177,55 +178,57 @@ void BeltSystem::reevaluateTunnelPairing()
|
|||||||
std::vector<TunnelLink> oldLinks;
|
std::vector<TunnelLink> oldLinks;
|
||||||
std::swap(oldLinks, m_tunnelLinks);
|
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)
|
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
|
||||||
{
|
{
|
||||||
const QPoint entryPos(entry.first.first, entry.first.second);
|
const QPoint entryPos(entry.first.first, entry.first.second);
|
||||||
const Rotation dir = entry.second.direction;
|
const Rotation dir = entry.second.direction;
|
||||||
const int maxDist = entry.second.maxDistance;
|
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;
|
continue;
|
||||||
for (int step = 0; step < d; ++step)
|
}
|
||||||
{
|
|
||||||
probe = adjacentTile(probe, dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a same-direction tunnel entry is here (blocks pairing)
|
TunnelLink link;
|
||||||
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
|
link.entryTile = entryPos;
|
||||||
m_tunnelEntries.find(key(probe));
|
link.exitTile = *target;
|
||||||
if (teIt != m_tunnelEntries.end() && teIt->second.direction == dir)
|
// 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;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ add_files(
|
|||||||
BeltSystemTest.cpp
|
BeltSystemTest.cpp
|
||||||
SurfaceMaskTest.cpp
|
SurfaceMaskTest.cpp
|
||||||
BeltDragPathTest.cpp
|
BeltDragPathTest.cpp
|
||||||
|
TunnelCompletionTest.cpp
|
||||||
BuildingTest.cpp
|
BuildingTest.cpp
|
||||||
BuildingConfigTest.cpp
|
BuildingConfigTest.cpp
|
||||||
ShipTest.cpp
|
ShipTest.cpp
|
||||||
|
|||||||
171
src/test/TunnelCompletionTest.cpp
Normal file
171
src/test/TunnelCompletionTest.cpp
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "TunnelCompletion.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
using TunnelMap = std::map<std::pair<int, int>, TunnelTileInfo>;
|
||||||
|
|
||||||
|
TunnelLookup makeLookup(const TunnelMap& tunnels)
|
||||||
|
{
|
||||||
|
return [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
|
||||||
|
{
|
||||||
|
const TunnelMap::const_iterator it = tunnels.find({tile.x(), tile.y()});
|
||||||
|
if (it == tunnels.end()) { return std::nullopt; }
|
||||||
|
return it->second;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor at the centre of a tile.
|
||||||
|
QVector2D tileCentre(QPoint tile)
|
||||||
|
{
|
||||||
|
return QVector2D(static_cast<float>(tile.x()) + 0.5f,
|
||||||
|
static_cast<float>(tile.y()) + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No match
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("No tunnels: ghost defaults to a Tunnel Entry with no partner")
|
||||||
|
{
|
||||||
|
const TunnelMap tunnels;
|
||||||
|
const TunnelCompletion result = resolveTunnelCompletion(
|
||||||
|
makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));
|
||||||
|
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(!result.partnerTile.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Entry-completion: an existing exit ahead keeps the ghost an entry
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Existing exit ahead completes as an entry")
|
||||||
|
{
|
||||||
|
TunnelMap tunnels;
|
||||||
|
tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East};
|
||||||
|
|
||||||
|
const TunnelCompletion result = resolveTunnelCompletion(
|
||||||
|
makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));
|
||||||
|
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(result.partnerTile.has_value());
|
||||||
|
REQUIRE(*result.partnerTile == QPoint(8, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Exit-completion: an existing entry behind flips the ghost to an exit
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Existing entry behind completes as an exit")
|
||||||
|
{
|
||||||
|
TunnelMap tunnels;
|
||||||
|
tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};
|
||||||
|
|
||||||
|
const TunnelCompletion result = resolveTunnelCompletion(
|
||||||
|
makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));
|
||||||
|
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelExit);
|
||||||
|
REQUIRE(result.partnerTile.has_value());
|
||||||
|
REQUIRE(*result.partnerTile == QPoint(2, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Both matches: the sub-tile cursor position breaks the tie
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Both matches resolve to the partner nearer the sub-tile cursor")
|
||||||
|
{
|
||||||
|
TunnelMap tunnels;
|
||||||
|
tunnels[{3, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // behind
|
||||||
|
tunnels[{7, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // ahead
|
||||||
|
|
||||||
|
const TunnelLookup lookup = makeLookup(tunnels);
|
||||||
|
|
||||||
|
SECTION("cursor near the left edge completes the entry (ghost becomes exit)")
|
||||||
|
{
|
||||||
|
const QVector2D cursor(5.1f, 5.5f);
|
||||||
|
const TunnelCompletion result =
|
||||||
|
resolveTunnelCompletion(lookup, QPoint(5, 5), Rotation::East, 10, cursor);
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelExit);
|
||||||
|
REQUIRE(*result.partnerTile == QPoint(3, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("cursor near the right edge completes the exit (ghost stays entry)")
|
||||||
|
{
|
||||||
|
const QVector2D cursor(5.9f, 5.5f);
|
||||||
|
const TunnelCompletion result =
|
||||||
|
resolveTunnelCompletion(lookup, QPoint(5, 5), Rotation::East, 10, cursor);
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(*result.partnerTile == QPoint(7, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// A same-direction tunnel between the ghost and a candidate blocks the pairing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("A same-direction entry between the ghost and an exit blocks completion")
|
||||||
|
{
|
||||||
|
TunnelMap tunnels;
|
||||||
|
tunnels[{7, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // blocker
|
||||||
|
tunnels[{9, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East};
|
||||||
|
|
||||||
|
const TunnelCompletion result = resolveTunnelCompletion(
|
||||||
|
makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));
|
||||||
|
|
||||||
|
// The forward scan stops at the same-direction entry, which is not an exit, so no
|
||||||
|
// entry-completion. Nothing behind, so no exit-completion either.
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(!result.partnerTile.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Differently-facing tunnels are skipped by the scan
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("A differently-facing tunnel ahead is skipped, not treated as a match")
|
||||||
|
{
|
||||||
|
TunnelMap tunnels;
|
||||||
|
tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::North}; // skipped
|
||||||
|
tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // match
|
||||||
|
|
||||||
|
const TunnelCompletion result = resolveTunnelCompletion(
|
||||||
|
makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));
|
||||||
|
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(*result.partnerTile == QPoint(8, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Distance limit
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("A candidate beyond the max distance is not matched")
|
||||||
|
{
|
||||||
|
TunnelMap tunnels;
|
||||||
|
tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // distance 3
|
||||||
|
|
||||||
|
const TunnelCompletion result = resolveTunnelCompletion(
|
||||||
|
makeLookup(tunnels), QPoint(5, 5), Rotation::East, 2, tileCentre(QPoint(5, 5)));
|
||||||
|
|
||||||
|
REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
|
||||||
|
REQUIRE(!result.partnerTile.has_value());
|
||||||
|
}
|
||||||
@@ -35,11 +35,21 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWid
|
|||||||
{
|
{
|
||||||
continue;
|
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_types.push_back(def.type);
|
||||||
m_costs[def.type] = def.cost;
|
m_costs[def.type] = def.cost;
|
||||||
|
|
||||||
const QString label = QString::fromStdString(toDisplayName(def.id))
|
const QString name = (def.type == BuildingType::TunnelEntry)
|
||||||
+ "\n" + tr("%1 Building Blocks").arg(def.cost);
|
? 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);
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
btn->setFixedHeight(48);
|
btn->setFixedHeight(48);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
#include "ScrapDataComponent.h"
|
#include "ScrapDataComponent.h"
|
||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TunnelCompletion.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "TracePrintRequestedEvent.h"
|
#include "TracePrintRequestedEvent.h"
|
||||||
#include "BuildHotkeyPressedEvent.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)
|
void GameWorldView::placeAtTile(QPoint tile)
|
||||||
{
|
{
|
||||||
if (!m_builderType.has_value())
|
if (!m_builderType.has_value())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const BuildingType type = *m_builderType;
|
const BuildingType type = effectiveBuilderType();
|
||||||
|
|
||||||
if (!isValidPlacement(type, tile, m_ghostRotation))
|
if (!isValidPlacement(type, tile, m_ghostRotation))
|
||||||
{
|
{
|
||||||
@@ -903,11 +959,11 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For placements whose UI follow-up depends on success (the tunnel entry/exit
|
// For the splitter and tunnels, pre-validate occupancy + affordability so the
|
||||||
// toggle), pre-validate occupancy + affordability so the optimistic UI update
|
// optimistic UI update matches what the deferred command will do — isValidPlacement
|
||||||
// matches what the deferred command will do — isValidPlacement (above) already
|
// (above) already covered terrain/bounds. In tunnel mode the resolved type (entry
|
||||||
// covered terrain/bounds. Belts are placed via the drag path (applyBeltDragPath),
|
// or exit) is placed as-is (REQ-BLD-TUNNEL-MODE); there is no post-placement toggle.
|
||||||
// not here.
|
// Belts are placed via the drag path (applyBeltDragPath), not here.
|
||||||
if (type == BuildingType::Splitter
|
if (type == BuildingType::Splitter
|
||||||
|| type == BuildingType::TunnelEntry
|
|| type == BuildingType::TunnelEntry
|
||||||
|| type == BuildingType::TunnelExit)
|
|| type == BuildingType::TunnelExit)
|
||||||
@@ -915,14 +971,6 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
|
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
|
||||||
{
|
{
|
||||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
||||||
if (type == BuildingType::TunnelEntry)
|
|
||||||
{
|
|
||||||
m_builderType = BuildingType::TunnelExit;
|
|
||||||
}
|
|
||||||
else if (type == BuildingType::TunnelExit)
|
|
||||||
{
|
|
||||||
m_builderType = BuildingType::TunnelEntry;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1798,7 +1846,27 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
}
|
}
|
||||||
else
|
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,
|
m_ghostRotation, m_ghostValid,
|
||||||
/*showPortTargetGlyphs*/ true);
|
/*showPortTargetGlyphs*/ true);
|
||||||
}
|
}
|
||||||
@@ -2099,8 +2167,7 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
{
|
{
|
||||||
case 1: type = BuildingType::Belt; break;
|
case 1: type = BuildingType::Belt; break;
|
||||||
case 2: type = BuildingType::Splitter; break;
|
case 2: type = BuildingType::Splitter; break;
|
||||||
case 3: type = BuildingType::TunnelEntry; break;
|
case 3: type = BuildingType::TunnelEntry; break; // unified tunnel mode (REQ-BLD-TUNNEL-MODE); 4 unused
|
||||||
case 4: type = BuildingType::TunnelExit; break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2412,6 +2479,13 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
|||||||
m_ghostTile = tile;
|
m_ghostTile = tile;
|
||||||
m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation);
|
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)
|
if (m_dragging)
|
||||||
{
|
{
|
||||||
// Belt drag: update the previewed path; placement happens on release
|
// 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)
|
m_ghostRotation = clockwise ? rotateClockwise(m_ghostRotation)
|
||||||
: rotateCounterClockwise(m_ghostRotation);
|
: rotateCounterClockwise(m_ghostRotation);
|
||||||
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, 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,
|
// Rotating during a belt drag re-picks the path's primary axis immediately,
|
||||||
// without waiting for the next mouse move (REQ-BLD-BELT-DRAG).
|
// without waiting for the next mouse move (REQ-BLD-BELT-DRAG).
|
||||||
if (m_dragging) { recomputeBeltDragPath(m_ghostTile); }
|
if (m_dragging) { recomputeBeltDragPath(m_ghostTile); }
|
||||||
@@ -2701,9 +2777,11 @@ void GameWorldView::pasteConfigTo(BuildingId id)
|
|||||||
|
|
||||||
void GameWorldView::enterBuilderMode(BuildingType type)
|
void GameWorldView::enterBuilderMode(BuildingType type)
|
||||||
{
|
{
|
||||||
m_builderType = type;
|
m_builderType = type;
|
||||||
m_ghostRotation = Rotation::East;
|
m_ghostRotation = Rotation::East;
|
||||||
m_ghostValid = false;
|
m_ghostValid = false;
|
||||||
|
m_tunnelGhostType = BuildingType::TunnelEntry;
|
||||||
|
m_tunnelPartnerTile.reset();
|
||||||
m_demolishMode = false;
|
m_demolishMode = false;
|
||||||
m_blueprintMode.reset();
|
m_blueprintMode.reset();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
|||||||
@@ -205,6 +205,16 @@ private:
|
|||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
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).
|
// Belt drag placement (REQ-BLD-BELT-DRAG).
|
||||||
// Per-path-tile decision, shared by ghost drawing and release-time placement.
|
// Per-path-tile decision, shared by ghost drawing and release-time placement.
|
||||||
enum class BeltTileAction { PlaceNew, RotateInPlace, Invalid };
|
enum class BeltTileAction { PlaceNew, RotateInPlace, Invalid };
|
||||||
@@ -274,6 +284,12 @@ private:
|
|||||||
Rotation m_ghostRotation;
|
Rotation m_ghostRotation;
|
||||||
QPoint m_ghostTile;
|
QPoint m_ghostTile;
|
||||||
bool m_ghostValid;
|
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
|
// Deferred belt drag placement (REQ-BLD-BELT-DRAG): while dragging, the
|
||||||
// rectilinear anchor->cursor path is recomputed on each move and only applied
|
// rectilinear anchor->cursor path is recomputed on each move and only applied
|
||||||
// on release. Empty unless a belt drag is in progress.
|
// on release. Empty unless a belt drag is in progress.
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ struct OverlayVisuals
|
|||||||
QColor copyConfig;
|
QColor copyConfig;
|
||||||
QColor lockedAsteroid;
|
QColor lockedAsteroid;
|
||||||
QColor modalDim;
|
QColor modalDim;
|
||||||
|
QColor tunnelPreview; // tunnel connection preview highlight (REQ-BLD-TUNNEL-MODE)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ToastVisuals
|
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.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config");
|
||||||
cfg.overlays.lockedAsteroid = parseColor(requireString(ov, "locked_asteroid", "overlays"), "overlays.locked_asteroid");
|
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.modalDim = parseColor(requireString(ov, "modal_dim", "overlays"), "overlays.modal_dim");
|
||||||
|
cfg.overlays.tunnelPreview = parseColor(requireString(ov, "tunnel_preview", "overlays"), "overlays.tunnel_preview");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toast
|
// Toast
|
||||||
|
|||||||
Reference in New Issue
Block a user