implement tunnels
This commit is contained in:
@@ -12,6 +12,21 @@ player_placeable = true
|
||||
construction_time_seconds = 1
|
||||
surface_mask = ["<A>"]
|
||||
|
||||
[[building]]
|
||||
id = "tunnel_entry"
|
||||
cost = 5
|
||||
player_placeable = true
|
||||
construction_time_seconds = 3
|
||||
surface_mask = ["A>"]
|
||||
tunnel_max_distance = 10
|
||||
|
||||
[[building]]
|
||||
id = "tunnel_exit"
|
||||
cost = 5
|
||||
player_placeable = true
|
||||
construction_time_seconds = 3
|
||||
surface_mask = ["A>"]
|
||||
|
||||
[[building]]
|
||||
id = "miner"
|
||||
cost = 15
|
||||
|
||||
@@ -72,6 +72,16 @@ fill = "#7a7a5a"
|
||||
outline = "#9a9a7a"
|
||||
glyph = ""
|
||||
|
||||
[buildings.tunnel_entry]
|
||||
fill = "#4a6a5a"
|
||||
outline = "#8aaa9a"
|
||||
glyph = "Te"
|
||||
|
||||
[buildings.tunnel_exit]
|
||||
fill = "#5a6a4a"
|
||||
outline = "#9aaa8a"
|
||||
glyph = "Tx"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stations
|
||||
#
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -18,6 +19,7 @@ struct BuildingDef
|
||||
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
||||
// happens when buildings are placed, not at load time.
|
||||
std::vector<std::string> surfaceMask;
|
||||
std::optional<int> tunnelMaxDistance;
|
||||
};
|
||||
|
||||
struct BuildingsConfig
|
||||
|
||||
@@ -275,6 +275,11 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
||||
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
|
||||
if (const std::optional<int64_t> tmd = mt["tunnel_max_distance"].value<int64_t>())
|
||||
{
|
||||
def.tunnelMaxDistance = static_cast<int>(*tmd);
|
||||
}
|
||||
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||
if (!parsedType)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,8 @@ std::optional<BuildingType> parseBuildingType(const std::string& id)
|
||||
if (id == "salvage_bay") { return BuildingType::SalvageBay; }
|
||||
if (id == "belt") { return BuildingType::Belt; }
|
||||
if (id == "splitter") { return BuildingType::Splitter; }
|
||||
if (id == "tunnel_entry") { return BuildingType::TunnelEntry; }
|
||||
if (id == "tunnel_exit") { return BuildingType::TunnelExit; }
|
||||
if (id == "hq") { return BuildingType::Hq; }
|
||||
if (id == "player_defence_station") { return BuildingType::PlayerDefenceStation; }
|
||||
if (id == "enemy_defence_station") { return BuildingType::EnemyDefenceStation; }
|
||||
@@ -28,6 +30,8 @@ std::string buildingTypeId(BuildingType type)
|
||||
case BuildingType::SalvageBay: return "salvage_bay";
|
||||
case BuildingType::Belt: return "belt";
|
||||
case BuildingType::Splitter: return "splitter";
|
||||
case BuildingType::TunnelEntry: return "tunnel_entry";
|
||||
case BuildingType::TunnelExit: return "tunnel_exit";
|
||||
case BuildingType::Hq: return "hq";
|
||||
case BuildingType::PlayerDefenceStation: return "player_defence_station";
|
||||
case BuildingType::EnemyDefenceStation: return "enemy_defence_station";
|
||||
|
||||
@@ -16,6 +16,8 @@ enum class BuildingType
|
||||
SalvageBay,
|
||||
Belt,
|
||||
Splitter,
|
||||
TunnelEntry,
|
||||
TunnelExit,
|
||||
Hq,
|
||||
PlayerDefenceStation,
|
||||
EnemyDefenceStation,
|
||||
|
||||
@@ -70,10 +70,39 @@ void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
|
||||
m_splitters[key(tile)] = st;
|
||||
}
|
||||
|
||||
void BeltSystem::removeTile(QPoint tile)
|
||||
void BeltSystem::placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance)
|
||||
{
|
||||
m_belts.erase(key(tile));
|
||||
m_splitters.erase(key(tile));
|
||||
m_tunnelExits.erase(key(tile));
|
||||
TunnelEntryTile te;
|
||||
te.direction = direction;
|
||||
te.maxDistance = maxDistance;
|
||||
m_tunnelEntries[key(tile)] = te;
|
||||
reevaluateTunnelPairing();
|
||||
}
|
||||
|
||||
void BeltSystem::placeTunnelExit(QPoint tile, Rotation direction)
|
||||
{
|
||||
m_belts.erase(key(tile));
|
||||
m_splitters.erase(key(tile));
|
||||
m_tunnelEntries.erase(key(tile));
|
||||
TunnelExitTile tx;
|
||||
tx.direction = direction;
|
||||
m_tunnelExits[key(tile)] = tx;
|
||||
reevaluateTunnelPairing();
|
||||
}
|
||||
|
||||
void BeltSystem::removeTile(QPoint tile)
|
||||
{
|
||||
const bool wasTunnel = (m_tunnelEntries.erase(key(tile)) > 0)
|
||||
| (m_tunnelExits.erase(key(tile)) > 0);
|
||||
m_belts.erase(key(tile));
|
||||
m_splitters.erase(key(tile));
|
||||
if (wasTunnel)
|
||||
{
|
||||
reevaluateTunnelPairing();
|
||||
}
|
||||
}
|
||||
|
||||
void BeltSystem::setSplitterFilters(QPoint tile,
|
||||
@@ -105,18 +134,98 @@ std::optional<BeltSystem::SplitterInfo> BeltSystem::getSplitterInfo(QPoint tile)
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tunnel pairing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void BeltSystem::reevaluateTunnelPairing()
|
||||
{
|
||||
std::vector<TunnelLink> oldLinks;
|
||||
std::swap(oldLinks, m_tunnelLinks);
|
||||
|
||||
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)
|
||||
{
|
||||
QPoint probe = entryPos;
|
||||
for (int step = 0; step < d; ++step)
|
||||
{
|
||||
probe = adjacentTile(probe, dir);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Port interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool BeltSystem::tryPutItem(QPoint tile, Item item)
|
||||
{
|
||||
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(tile));
|
||||
if (it == m_belts.end())
|
||||
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
|
||||
if (bIt != m_belts.end())
|
||||
{
|
||||
return tryPlaceOnBelt(tile, item);
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
|
||||
m_tunnelEntries.find(key(tile));
|
||||
if (teIt != m_tunnelEntries.end())
|
||||
{
|
||||
TunnelEntryTile& te = teIt->second;
|
||||
if (!te.front)
|
||||
{
|
||||
te.front = BeltItemSlot{item, 0.0};
|
||||
return true;
|
||||
}
|
||||
if (!te.back)
|
||||
{
|
||||
te.back = BeltItemSlot{item, 0.0};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return tryPlaceOnBelt(tile, item);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
||||
@@ -158,6 +267,20 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
|
||||
m_tunnelExits.find(key(port.tile));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
TunnelExitTile& tx = txIt->second;
|
||||
if (tx.direction == port.direction && tx.front && tx.front->progress >= 1.0)
|
||||
{
|
||||
const Item taken = tx.front->item;
|
||||
tx.front = tx.back;
|
||||
tx.back = std::nullopt;
|
||||
return taken;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -194,6 +317,17 @@ std::optional<ItemType> BeltSystem::peekItem(Port port) const
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
|
||||
m_tunnelExits.find(key(port.tile));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
const TunnelExitTile& tx = txIt->second;
|
||||
if (tx.direction == port.direction && tx.front && tx.front->progress >= 1.0)
|
||||
{
|
||||
return tx.front->item.type;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -219,6 +353,36 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
|
||||
sIt->second.frontA = std::nullopt;
|
||||
sIt->second.frontB = std::nullopt;
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
|
||||
m_tunnelEntries.find(key(tile));
|
||||
if (teIt != m_tunnelEntries.end())
|
||||
{
|
||||
teIt->second.front = std::nullopt;
|
||||
teIt->second.back = std::nullopt;
|
||||
for (TunnelLink& link : m_tunnelLinks)
|
||||
{
|
||||
if (link.entryTile == tile)
|
||||
{
|
||||
link.items.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
|
||||
m_tunnelExits.find(key(tile));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
txIt->second.front = std::nullopt;
|
||||
txIt->second.back = std::nullopt;
|
||||
for (TunnelLink& link : m_tunnelLinks)
|
||||
{
|
||||
if (link.exitTile == tile)
|
||||
{
|
||||
link.items.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +393,9 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
|
||||
void BeltSystem::tick()
|
||||
{
|
||||
advanceProgress();
|
||||
advanceTunnelProgress();
|
||||
moveItemsToNextTile();
|
||||
moveTunnelItems();
|
||||
routeSplitterItems();
|
||||
}
|
||||
|
||||
@@ -304,6 +470,98 @@ void BeltSystem::advanceProgress()
|
||||
}
|
||||
}
|
||||
|
||||
void BeltSystem::advanceTunnelProgress()
|
||||
{
|
||||
for (std::map<std::pair<int, int>, TunnelEntryTile>::iterator it = m_tunnelEntries.begin();
|
||||
it != m_tunnelEntries.end(); ++it)
|
||||
{
|
||||
TunnelEntryTile& te = it->second;
|
||||
|
||||
if (te.front)
|
||||
{
|
||||
te.front->progress += m_progressPerTick;
|
||||
if (te.front->progress > 1.0)
|
||||
{
|
||||
te.front->progress = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
if (te.back)
|
||||
{
|
||||
te.back->progress += m_progressPerTick;
|
||||
if (te.front && te.back->progress >= te.front->progress)
|
||||
{
|
||||
te.back->progress = te.front->progress - m_progressPerTick;
|
||||
if (te.back->progress < 0.0)
|
||||
{
|
||||
te.back->progress = 0.0;
|
||||
}
|
||||
}
|
||||
if (te.back->progress > 0.5)
|
||||
{
|
||||
te.back->progress = 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
|
||||
it != m_tunnelExits.end(); ++it)
|
||||
{
|
||||
TunnelExitTile& tx = it->second;
|
||||
|
||||
if (tx.front)
|
||||
{
|
||||
tx.front->progress += m_progressPerTick;
|
||||
if (tx.front->progress > 1.0)
|
||||
{
|
||||
tx.front->progress = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
if (tx.back)
|
||||
{
|
||||
tx.back->progress += m_progressPerTick;
|
||||
if (tx.front && tx.back->progress >= tx.front->progress)
|
||||
{
|
||||
tx.back->progress = tx.front->progress - m_progressPerTick;
|
||||
if (tx.back->progress < 0.0)
|
||||
{
|
||||
tx.back->progress = 0.0;
|
||||
}
|
||||
}
|
||||
if (tx.back->progress > 0.5)
|
||||
{
|
||||
tx.back->progress = 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (TunnelLink& link : m_tunnelLinks)
|
||||
{
|
||||
for (std::size_t i = 0; i < link.items.size(); ++i)
|
||||
{
|
||||
TunnelTransitItem& ti = link.items[i];
|
||||
ti.progress += m_progressPerTick;
|
||||
if (ti.progress > link.length)
|
||||
{
|
||||
ti.progress = link.length;
|
||||
}
|
||||
if (i > 0)
|
||||
{
|
||||
const double maxProgress = link.items[i - 1].progress - 0.5;
|
||||
if (ti.progress > maxProgress)
|
||||
{
|
||||
ti.progress = maxProgress;
|
||||
if (ti.progress < 0.0)
|
||||
{
|
||||
ti.progress = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BeltSystem::moveItemsToNextTile()
|
||||
{
|
||||
// Belt items advancing into the next tile.
|
||||
@@ -340,11 +598,25 @@ void BeltSystem::moveItemsToNextTile()
|
||||
bt.front = bt.back;
|
||||
bt.back = std::nullopt;
|
||||
}
|
||||
// else: splitter back occupied — item stays blocked at progress 1.0.
|
||||
}
|
||||
// else: no tile registered (e.g. open space, or building input port).
|
||||
// Items leaving into unregistered tiles are not consumed here — the
|
||||
// building pull step uses tryTakeItem for that.
|
||||
else
|
||||
{
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator nextEntry =
|
||||
m_tunnelEntries.find(key(next));
|
||||
if (nextEntry != m_tunnelEntries.end() && !nextEntry->second.back)
|
||||
{
|
||||
if (!nextEntry->second.front)
|
||||
{
|
||||
nextEntry->second.front = BeltItemSlot{bt.front->item, 0.0};
|
||||
}
|
||||
else
|
||||
{
|
||||
nextEntry->second.back = BeltItemSlot{bt.front->item, 0.0};
|
||||
}
|
||||
bt.front = bt.back;
|
||||
bt.back = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Splitter front slots advancing into downstream belt tiles.
|
||||
@@ -357,23 +629,89 @@ void BeltSystem::moveItemsToNextTile()
|
||||
if (st.frontA && st.frontA->progress >= 1.0)
|
||||
{
|
||||
const QPoint dest = adjacentTile(here, st.outputA);
|
||||
if (tryPlaceOnBelt(dest, st.frontA->item))
|
||||
if (tryPushToTile(dest, st.frontA->item, st.outputA))
|
||||
{
|
||||
st.frontA = std::nullopt;
|
||||
}
|
||||
// else: downstream belt full or absent — item stays at progress 1.0
|
||||
// for building pickup via tryTakeItem.
|
||||
}
|
||||
|
||||
if (st.frontB && st.frontB->progress >= 1.0)
|
||||
{
|
||||
const QPoint dest = adjacentTile(here, st.outputB);
|
||||
if (tryPlaceOnBelt(dest, st.frontB->item))
|
||||
if (tryPushToTile(dest, st.frontB->item, st.outputB))
|
||||
{
|
||||
st.frontB = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tunnel exit items advancing into downstream tiles.
|
||||
for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
|
||||
it != m_tunnelExits.end(); ++it)
|
||||
{
|
||||
TunnelExitTile& tx = it->second;
|
||||
if (!tx.front || tx.front->progress < 1.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const QPoint here = QPoint(it->first.first, it->first.second);
|
||||
const QPoint next = adjacentTile(here, tx.direction);
|
||||
if (tryPushToTile(next, tx.front->item, tx.direction))
|
||||
{
|
||||
tx.front = tx.back;
|
||||
tx.back = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BeltSystem::moveTunnelItems()
|
||||
{
|
||||
for (TunnelLink& link : m_tunnelLinks)
|
||||
{
|
||||
// Entry front → transit
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
|
||||
m_tunnelEntries.find(key(link.entryTile));
|
||||
if (teIt != m_tunnelEntries.end())
|
||||
{
|
||||
TunnelEntryTile& te = teIt->second;
|
||||
if (te.front && te.front->progress >= 1.0)
|
||||
{
|
||||
const bool canEnter = link.items.empty()
|
||||
|| link.items.back().progress >= 0.5;
|
||||
if (canEnter)
|
||||
{
|
||||
TunnelTransitItem ti;
|
||||
ti.item = te.front->item;
|
||||
ti.progress = 0.0;
|
||||
link.items.push_back(ti);
|
||||
te.front = te.back;
|
||||
te.back = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transit front → exit back
|
||||
if (!link.items.empty() && link.items.front().progress >= link.length)
|
||||
{
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
|
||||
m_tunnelExits.find(key(link.exitTile));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
TunnelExitTile& tx = txIt->second;
|
||||
if (!tx.back && !tx.front)
|
||||
{
|
||||
tx.front = BeltItemSlot{link.items.front().item, 0.0};
|
||||
link.items.erase(link.items.begin());
|
||||
}
|
||||
else if (!tx.back && tx.front)
|
||||
{
|
||||
tx.back = BeltItemSlot{link.items.front().item, 0.0};
|
||||
link.items.erase(link.items.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BeltSystem::routeSplitterItems()
|
||||
@@ -474,6 +812,65 @@ bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item)
|
||||
return false; // both slots occupied
|
||||
}
|
||||
|
||||
bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
|
||||
{
|
||||
if (tryPlaceOnBelt(dest, item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
|
||||
m_splitters.find(key(dest));
|
||||
if (splIt != m_splitters.end())
|
||||
{
|
||||
if (!splIt->second.back)
|
||||
{
|
||||
splIt->second.back = BeltItemSlot{item, 0.0};
|
||||
splIt->second.backDir = fromDir;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
|
||||
m_tunnelEntries.find(key(dest));
|
||||
if (teIt != m_tunnelEntries.end())
|
||||
{
|
||||
TunnelEntryTile& te = teIt->second;
|
||||
if (!te.front)
|
||||
{
|
||||
te.front = BeltItemSlot{item, 0.0};
|
||||
return true;
|
||||
}
|
||||
if (!te.back)
|
||||
{
|
||||
te.back = BeltItemSlot{item, 0.0};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
|
||||
m_tunnelExits.find(key(dest));
|
||||
if (txIt != m_tunnelExits.end())
|
||||
{
|
||||
TunnelExitTile& tx = txIt->second;
|
||||
if (!tx.front)
|
||||
{
|
||||
tx.front = BeltItemSlot{item, 0.0};
|
||||
return true;
|
||||
}
|
||||
if (!tx.back)
|
||||
{
|
||||
tx.back = BeltItemSlot{item, 0.0};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -542,4 +939,54 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
visit(vi);
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
|
||||
{
|
||||
const QPoint tile(entry.first.first, entry.first.second);
|
||||
if (!viewportTiles.contains(tile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const TunnelEntryTile& te = entry.second;
|
||||
if (te.front)
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = te.front->item.type;
|
||||
vi.worldPos = slotWorldPos(tile, te.direction, te.front->progress);
|
||||
visit(vi);
|
||||
}
|
||||
if (te.back)
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = te.back->item.type;
|
||||
vi.worldPos = slotWorldPos(tile, te.direction, te.back->progress);
|
||||
visit(vi);
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::pair<const std::pair<int, int>, TunnelExitTile>& entry : m_tunnelExits)
|
||||
{
|
||||
const QPoint tile(entry.first.first, entry.first.second);
|
||||
if (!viewportTiles.contains(tile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const TunnelExitTile& tx = entry.second;
|
||||
if (tx.front)
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = tx.front->item.type;
|
||||
vi.worldPos = slotWorldPos(tile, tx.direction, tx.front->progress);
|
||||
visit(vi);
|
||||
}
|
||||
if (tx.back)
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = tx.back->item.type;
|
||||
vi.worldPos = slotWorldPos(tile, tx.direction, tx.back->progress);
|
||||
visit(vi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ public:
|
||||
// Register a new belt tile. Any items already on this tile are cleared.
|
||||
void placeBelt(QPoint tile, Rotation direction);
|
||||
|
||||
// Register a new tunnel entry tile.
|
||||
void placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance);
|
||||
|
||||
// Register a new tunnel exit tile.
|
||||
void placeTunnelExit(QPoint tile, Rotation direction);
|
||||
|
||||
// Register a new splitter tile. outputA and outputB are the two exit
|
||||
// directions (e.g. West and East for a default-rotation splitter).
|
||||
// Items entering from any adjacent belt whose direction points into this
|
||||
@@ -87,13 +93,20 @@ public:
|
||||
|
||||
private:
|
||||
void advanceProgress();
|
||||
void advanceTunnelProgress();
|
||||
void moveItemsToNextTile();
|
||||
void moveTunnelItems();
|
||||
void routeSplitterItems();
|
||||
|
||||
// Place item into back slot of an existing belt tile at progress 0.
|
||||
// Returns false if tile is not a belt or is full.
|
||||
bool tryPlaceOnBelt(QPoint tile, Item item);
|
||||
|
||||
// Push an item to any tile type (belt, splitter, tunnel entry, tunnel exit).
|
||||
bool tryPushToTile(QPoint dest, Item item, Rotation fromDir);
|
||||
|
||||
void reevaluateTunnelPairing();
|
||||
|
||||
static std::pair<int, int> key(QPoint tile);
|
||||
static QPoint adjacentTile(QPoint tile, Rotation dir);
|
||||
|
||||
@@ -126,9 +139,41 @@ private:
|
||||
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
|
||||
};
|
||||
|
||||
struct TunnelEntryTile
|
||||
{
|
||||
Rotation direction;
|
||||
int maxDistance;
|
||||
std::optional<BeltItemSlot> front;
|
||||
std::optional<BeltItemSlot> back;
|
||||
};
|
||||
|
||||
struct TunnelExitTile
|
||||
{
|
||||
Rotation direction;
|
||||
std::optional<BeltItemSlot> front;
|
||||
std::optional<BeltItemSlot> back;
|
||||
};
|
||||
|
||||
struct TunnelTransitItem
|
||||
{
|
||||
Item item;
|
||||
double progress;
|
||||
};
|
||||
|
||||
struct TunnelLink
|
||||
{
|
||||
QPoint entryTile;
|
||||
QPoint exitTile;
|
||||
double length;
|
||||
std::vector<TunnelTransitItem> items; // front (highest progress) to back
|
||||
};
|
||||
|
||||
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
|
||||
|
||||
std::map<std::pair<int, int>, BeltTile> m_belts;
|
||||
std::map<std::pair<int, int>, SplitterTile> m_splitters;
|
||||
std::map<std::pair<int, int>, BeltTile> m_belts;
|
||||
std::map<std::pair<int, int>, SplitterTile> m_splitters;
|
||||
std::map<std::pair<int, int>, TunnelEntryTile> m_tunnelEntries;
|
||||
std::map<std::pair<int, int>, TunnelExitTile> m_tunnelExits;
|
||||
std::vector<TunnelLink> m_tunnelLinks;
|
||||
};
|
||||
|
||||
|
||||
@@ -269,7 +269,8 @@ int BuildingSystem::demolish(EntityId id)
|
||||
{
|
||||
if (it->id == id)
|
||||
{
|
||||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
|
||||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|
||||
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
|
||||
{
|
||||
m_belts.removeTile(it->anchor);
|
||||
}
|
||||
@@ -424,6 +425,16 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
mask.outputPorts[0].direction,
|
||||
mask.outputPorts[1].direction);
|
||||
}
|
||||
else if (front.type == BuildingType::TunnelEntry)
|
||||
{
|
||||
const BuildingDef* bdef = findBuildingDef(front.type);
|
||||
const int maxDist = (bdef && bdef->tunnelMaxDistance) ? *bdef->tunnelMaxDistance : 0;
|
||||
m_belts.placeTunnelEntry(front.anchor, front.rotation, maxDist);
|
||||
}
|
||||
else if (front.type == BuildingType::TunnelExit)
|
||||
{
|
||||
m_belts.placeTunnelExit(front.anchor, front.rotation);
|
||||
}
|
||||
|
||||
m_buildings.push_back(std::move(building));
|
||||
|
||||
@@ -902,7 +913,8 @@ bool BuildingSystem::removeBuilding(EntityId id)
|
||||
{
|
||||
if (it->id == id)
|
||||
{
|
||||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
|
||||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|
||||
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
|
||||
{
|
||||
m_belts.removeTile(it->anchor);
|
||||
}
|
||||
|
||||
@@ -660,3 +660,257 @@ TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (bui
|
||||
bs.tick();
|
||||
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tunnel — pairing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("BeltSystem: tunnel pairing — basic pair within max distance", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
const QPoint entry(0, 0);
|
||||
const QPoint exit(3, 0);
|
||||
|
||||
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
||||
bs.placeTunnelExit(exit, Rotation::East);
|
||||
|
||||
bs.tryPutItem(entry, makeItem("iron_ore"));
|
||||
|
||||
// With kFastBeltSpeed, items cross one tile per tick.
|
||||
// entry tile: 1 tick to reach front progress 1.0
|
||||
// transit: 3 tiles distance → 3 ticks
|
||||
// exit tile: 1 tick to reach front progress 1.0
|
||||
// Total: 1 (entry) + 1 (entry→transit) + 3 (transit) + 1 (transit→exit) + 1 (exit advance) = ~5-7 ticks
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
REQUIRE(bs.peekItem(Port{exit, Rotation::East}).has_value());
|
||||
const std::optional<Item> taken = bs.tryTakeItem(Port{exit, Rotation::East});
|
||||
REQUIRE(taken.has_value());
|
||||
REQUIRE(taken->type.id == "iron_ore");
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: tunnel pairing — wrong direction prevents pair", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
||||
bs.placeTunnelExit(QPoint(3, 0), Rotation::North);
|
||||
|
||||
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
// Exit faces North, not East — no pair formed, item stuck in entry.
|
||||
REQUIRE_FALSE(bs.peekItem(Port{QPoint(3, 0), Rotation::North}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: tunnel pairing — beyond max distance prevents pair", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 2);
|
||||
bs.placeTunnelExit(QPoint(3, 0), Rotation::East);
|
||||
|
||||
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
REQUIRE_FALSE(bs.peekItem(Port{QPoint(3, 0), Rotation::East}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: tunnel pairing — same-dir entry between blocks pairing", "[belt]")
|
||||
{
|
||||
// Entry1 at (0,0) East, Entry2 at (2,0) East, Exit at (4,0) East.
|
||||
// Entry2 is closer to Exit → Entry2 pairs with Exit; Entry1 is blocked by Entry2.
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
||||
bs.placeTunnelEntry(QPoint(2, 0), Rotation::East, 10);
|
||||
bs.placeTunnelExit(QPoint(4, 0), Rotation::East);
|
||||
|
||||
// Put item on Entry2 — should reach exit.
|
||||
bs.tryPutItem(QPoint(2, 0), makeItem("copper_ore"));
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
REQUIRE(bs.peekItem(Port{QPoint(4, 0), Rotation::East}).has_value());
|
||||
bs.tryTakeItem(Port{QPoint(4, 0), Rotation::East});
|
||||
|
||||
// Put item on Entry1 — should NOT reach exit (Entry1 is unpaired).
|
||||
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
REQUIRE_FALSE(bs.peekItem(Port{QPoint(4, 0), Rotation::East}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: tunnel pairing — cross-dir entry between is ignored", "[belt]")
|
||||
{
|
||||
// Entry1 at (0,0) East, Entry2 at (2,0) North (different dir), Exit at (4,0) East.
|
||||
// Entry2 faces North → ignored → Entry1 pairs with Exit.
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
||||
bs.placeTunnelEntry(QPoint(2, 0), Rotation::North, 10);
|
||||
bs.placeTunnelExit(QPoint(4, 0), Rotation::East);
|
||||
|
||||
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
REQUIRE(bs.peekItem(Port{QPoint(4, 0), Rotation::East}).has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tunnel — item transit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("BeltSystem: unpaired entry blocks items at front", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
||||
// No exit placed — entry is unpaired.
|
||||
|
||||
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
// Item should not vanish — it stays in the entry.
|
||||
// We can verify by placing an exit and seeing item eventually arrive.
|
||||
bs.placeTunnelExit(QPoint(3, 0), Rotation::East);
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
REQUIRE(bs.peekItem(Port{QPoint(3, 0), Rotation::East}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: demolish entry discards transit items", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
const QPoint entry(0, 0);
|
||||
const QPoint exit(5, 0);
|
||||
|
||||
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
||||
bs.placeTunnelExit(exit, Rotation::East);
|
||||
|
||||
bs.tryPutItem(entry, makeItem("iron_ore"));
|
||||
|
||||
// Advance just enough for item to enter transit but not reach exit.
|
||||
bs.tick(); // item enters entry front
|
||||
bs.tick(); // entry front → transit (progress 0)
|
||||
|
||||
bs.removeTile(entry);
|
||||
|
||||
// Even with many more ticks, nothing arrives at exit.
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: clearTiles discards tunnel transit items", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
const QPoint entry(0, 0);
|
||||
const QPoint exit(5, 0);
|
||||
|
||||
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
||||
bs.placeTunnelExit(exit, Rotation::East);
|
||||
|
||||
bs.tryPutItem(entry, makeItem("iron_ore"));
|
||||
bs.tick();
|
||||
bs.tick();
|
||||
|
||||
bs.clearTiles({entry});
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: belt to entry to transit to exit to belt full chain", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
const QPoint beltIn(0, 0);
|
||||
const QPoint entry(1, 0);
|
||||
const QPoint exit(4, 0);
|
||||
const QPoint beltOut(5, 0);
|
||||
|
||||
bs.placeBelt(beltIn, Rotation::East);
|
||||
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
||||
bs.placeTunnelExit(exit, Rotation::East);
|
||||
bs.placeBelt(beltOut, Rotation::East);
|
||||
|
||||
bs.tryPutItem(beltIn, makeItem("iron_ore"));
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
// Item should have arrived on beltOut.
|
||||
REQUIRE(bs.peekItem(eastPort(beltOut)).has_value());
|
||||
const std::optional<Item> taken = bs.tryTakeItem(eastPort(beltOut));
|
||||
REQUIRE(taken.has_value());
|
||||
REQUIRE(taken->type.id == "iron_ore");
|
||||
}
|
||||
|
||||
TEST_CASE("BeltSystem: multiple items transit tunnel in order", "[belt]")
|
||||
{
|
||||
BeltSystem bs(kFastBeltSpeed);
|
||||
|
||||
const QPoint entry(0, 0);
|
||||
const QPoint exit(3, 0);
|
||||
|
||||
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
||||
bs.placeTunnelExit(exit, Rotation::East);
|
||||
|
||||
bs.tryPutItem(entry, makeItem("item1"));
|
||||
bs.tick();
|
||||
bs.tick(); // item1 enters transit
|
||||
|
||||
bs.tryPutItem(entry, makeItem("item2"));
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
// item1 should arrive first.
|
||||
const std::optional<Item> taken1 = bs.tryTakeItem(Port{exit, Rotation::East});
|
||||
REQUIRE(taken1.has_value());
|
||||
REQUIRE(taken1->type.id == "item1");
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
bs.tick();
|
||||
}
|
||||
|
||||
const std::optional<Item> taken2 = bs.tryTakeItem(Port{exit, Rotation::East});
|
||||
REQUIRE(taken2.has_value());
|
||||
REQUIRE(taken2->type.id == "item2");
|
||||
}
|
||||
|
||||
@@ -481,7 +481,9 @@ void GameWorldView::placeAtTile(QPoint tile)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == BuildingType::Splitter)
|
||||
else if (type == BuildingType::Splitter
|
||||
|| type == BuildingType::TunnelEntry
|
||||
|| type == BuildingType::TunnelExit)
|
||||
{
|
||||
if (!m_sim->buildings().isTileOccupied(tile))
|
||||
{
|
||||
|
||||
@@ -59,7 +59,8 @@ bool isProductionBuilding(BuildingType type)
|
||||
|
||||
bool isBeltLike(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Belt || type == BuildingType::Splitter;
|
||||
return type == BuildingType::Belt || type == BuildingType::Splitter
|
||||
|| type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit;
|
||||
}
|
||||
|
||||
QString rotationLabel(Rotation r)
|
||||
|
||||
@@ -124,6 +124,8 @@ const BuildingEntry kBuildingEntries[] = {
|
||||
{ "salvage_bay", BuildingType::SalvageBay },
|
||||
{ "belt", BuildingType::Belt },
|
||||
{ "splitter", BuildingType::Splitter },
|
||||
{ "tunnel_entry", BuildingType::TunnelEntry },
|
||||
{ "tunnel_exit", BuildingType::TunnelExit },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user