implement tunnels
This commit is contained in:
@@ -12,6 +12,21 @@ player_placeable = true
|
|||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
surface_mask = ["<A>"]
|
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]]
|
[[building]]
|
||||||
id = "miner"
|
id = "miner"
|
||||||
cost = 15
|
cost = 15
|
||||||
|
|||||||
@@ -72,6 +72,16 @@ fill = "#7a7a5a"
|
|||||||
outline = "#9a9a7a"
|
outline = "#9a9a7a"
|
||||||
glyph = ""
|
glyph = ""
|
||||||
|
|
||||||
|
[buildings.tunnel_entry]
|
||||||
|
fill = "#4a6a5a"
|
||||||
|
outline = "#8aaa9a"
|
||||||
|
glyph = "Te"
|
||||||
|
|
||||||
|
[buildings.tunnel_exit]
|
||||||
|
fill = "#5a6a4a"
|
||||||
|
outline = "#9aaa8a"
|
||||||
|
glyph = "Tx"
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Stations
|
# Stations
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ struct BuildingDef
|
|||||||
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
||||||
// happens when buildings are placed, not at load time.
|
// happens when buildings are placed, not at load time.
|
||||||
std::vector<std::string> surfaceMask;
|
std::vector<std::string> surfaceMask;
|
||||||
|
std::optional<int> tunnelMaxDistance;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BuildingsConfig
|
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.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
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);
|
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||||
if (!parsedType)
|
if (!parsedType)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ std::optional<BuildingType> parseBuildingType(const std::string& id)
|
|||||||
if (id == "salvage_bay") { return BuildingType::SalvageBay; }
|
if (id == "salvage_bay") { return BuildingType::SalvageBay; }
|
||||||
if (id == "belt") { return BuildingType::Belt; }
|
if (id == "belt") { return BuildingType::Belt; }
|
||||||
if (id == "splitter") { return BuildingType::Splitter; }
|
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 == "hq") { return BuildingType::Hq; }
|
||||||
if (id == "player_defence_station") { return BuildingType::PlayerDefenceStation; }
|
if (id == "player_defence_station") { return BuildingType::PlayerDefenceStation; }
|
||||||
if (id == "enemy_defence_station") { return BuildingType::EnemyDefenceStation; }
|
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::SalvageBay: return "salvage_bay";
|
||||||
case BuildingType::Belt: return "belt";
|
case BuildingType::Belt: return "belt";
|
||||||
case BuildingType::Splitter: return "splitter";
|
case BuildingType::Splitter: return "splitter";
|
||||||
|
case BuildingType::TunnelEntry: return "tunnel_entry";
|
||||||
|
case BuildingType::TunnelExit: return "tunnel_exit";
|
||||||
case BuildingType::Hq: return "hq";
|
case BuildingType::Hq: return "hq";
|
||||||
case BuildingType::PlayerDefenceStation: return "player_defence_station";
|
case BuildingType::PlayerDefenceStation: return "player_defence_station";
|
||||||
case BuildingType::EnemyDefenceStation: return "enemy_defence_station";
|
case BuildingType::EnemyDefenceStation: return "enemy_defence_station";
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ enum class BuildingType
|
|||||||
SalvageBay,
|
SalvageBay,
|
||||||
Belt,
|
Belt,
|
||||||
Splitter,
|
Splitter,
|
||||||
|
TunnelEntry,
|
||||||
|
TunnelExit,
|
||||||
Hq,
|
Hq,
|
||||||
PlayerDefenceStation,
|
PlayerDefenceStation,
|
||||||
EnemyDefenceStation,
|
EnemyDefenceStation,
|
||||||
|
|||||||
@@ -70,10 +70,39 @@ void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
|
|||||||
m_splitters[key(tile)] = st;
|
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_belts.erase(key(tile));
|
||||||
m_splitters.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,
|
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
|
// Port interface
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
bool BeltSystem::tryPutItem(QPoint tile, Item item)
|
bool BeltSystem::tryPutItem(QPoint tile, Item item)
|
||||||
{
|
{
|
||||||
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(tile));
|
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
|
||||||
if (it == m_belts.end())
|
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 false;
|
||||||
}
|
}
|
||||||
return tryPlaceOnBelt(tile, item);
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
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;
|
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;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +353,36 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
|
|||||||
sIt->second.frontA = std::nullopt;
|
sIt->second.frontA = std::nullopt;
|
||||||
sIt->second.frontB = 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()
|
void BeltSystem::tick()
|
||||||
{
|
{
|
||||||
advanceProgress();
|
advanceProgress();
|
||||||
|
advanceTunnelProgress();
|
||||||
moveItemsToNextTile();
|
moveItemsToNextTile();
|
||||||
|
moveTunnelItems();
|
||||||
routeSplitterItems();
|
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()
|
void BeltSystem::moveItemsToNextTile()
|
||||||
{
|
{
|
||||||
// Belt items advancing into the next tile.
|
// Belt items advancing into the next tile.
|
||||||
@@ -340,11 +598,25 @@ void BeltSystem::moveItemsToNextTile()
|
|||||||
bt.front = bt.back;
|
bt.front = bt.back;
|
||||||
bt.back = std::nullopt;
|
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).
|
else
|
||||||
// Items leaving into unregistered tiles are not consumed here — the
|
{
|
||||||
// building pull step uses tryTakeItem for that.
|
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.
|
// Splitter front slots advancing into downstream belt tiles.
|
||||||
@@ -357,23 +629,89 @@ void BeltSystem::moveItemsToNextTile()
|
|||||||
if (st.frontA && st.frontA->progress >= 1.0)
|
if (st.frontA && st.frontA->progress >= 1.0)
|
||||||
{
|
{
|
||||||
const QPoint dest = adjacentTile(here, st.outputA);
|
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;
|
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)
|
if (st.frontB && st.frontB->progress >= 1.0)
|
||||||
{
|
{
|
||||||
const QPoint dest = adjacentTile(here, st.outputB);
|
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;
|
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()
|
void BeltSystem::routeSplitterItems()
|
||||||
@@ -474,6 +812,65 @@ bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item)
|
|||||||
return false; // both slots occupied
|
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
|
// Rendering
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -542,4 +939,54 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
|||||||
visit(vi);
|
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.
|
// Register a new belt tile. Any items already on this tile are cleared.
|
||||||
void placeBelt(QPoint tile, Rotation direction);
|
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
|
// Register a new splitter tile. outputA and outputB are the two exit
|
||||||
// directions (e.g. West and East for a default-rotation splitter).
|
// directions (e.g. West and East for a default-rotation splitter).
|
||||||
// Items entering from any adjacent belt whose direction points into this
|
// Items entering from any adjacent belt whose direction points into this
|
||||||
@@ -87,13 +93,20 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void advanceProgress();
|
void advanceProgress();
|
||||||
|
void advanceTunnelProgress();
|
||||||
void moveItemsToNextTile();
|
void moveItemsToNextTile();
|
||||||
|
void moveTunnelItems();
|
||||||
void routeSplitterItems();
|
void routeSplitterItems();
|
||||||
|
|
||||||
// Place item into back slot of an existing belt tile at progress 0.
|
// Place item into back slot of an existing belt tile at progress 0.
|
||||||
// Returns false if tile is not a belt or is full.
|
// Returns false if tile is not a belt or is full.
|
||||||
bool tryPlaceOnBelt(QPoint tile, Item item);
|
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 std::pair<int, int> key(QPoint tile);
|
||||||
static QPoint adjacentTile(QPoint tile, Rotation dir);
|
static QPoint adjacentTile(QPoint tile, Rotation dir);
|
||||||
|
|
||||||
@@ -126,9 +139,41 @@ private:
|
|||||||
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
|
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
|
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
|
||||||
|
|
||||||
std::map<std::pair<int, int>, BeltTile> m_belts;
|
std::map<std::pair<int, int>, BeltTile> m_belts;
|
||||||
std::map<std::pair<int, int>, SplitterTile> m_splitters;
|
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->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);
|
m_belts.removeTile(it->anchor);
|
||||||
}
|
}
|
||||||
@@ -424,6 +425,16 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
mask.outputPorts[0].direction,
|
mask.outputPorts[0].direction,
|
||||||
mask.outputPorts[1].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));
|
m_buildings.push_back(std::move(building));
|
||||||
|
|
||||||
@@ -902,7 +913,8 @@ bool BuildingSystem::removeBuilding(EntityId id)
|
|||||||
{
|
{
|
||||||
if (it->id == 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);
|
m_belts.removeTile(it->anchor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -660,3 +660,257 @@ TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (bui
|
|||||||
bs.tick();
|
bs.tick();
|
||||||
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
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))
|
if (!m_sim->buildings().isTileOccupied(tile))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ bool isProductionBuilding(BuildingType type)
|
|||||||
|
|
||||||
bool isBeltLike(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)
|
QString rotationLabel(Rotation r)
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ const BuildingEntry kBuildingEntries[] = {
|
|||||||
{ "salvage_bay", BuildingType::SalvageBay },
|
{ "salvage_bay", BuildingType::SalvageBay },
|
||||||
{ "belt", BuildingType::Belt },
|
{ "belt", BuildingType::Belt },
|
||||||
{ "splitter", BuildingType::Splitter },
|
{ "splitter", BuildingType::Splitter },
|
||||||
|
{ "tunnel_entry", BuildingType::TunnelEntry },
|
||||||
|
{ "tunnel_exit", BuildingType::TunnelExit },
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user